Skip to content

Fix: No Routes Matched Location in React Router v6

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix 'No routes matched location' in React Router v6 — caused by missing Routes wrapper, wrong path syntax, nested route mismatches, and v5 to v6 migration issues.

The Error

You set up React Router v6 and the page renders blank, or you see this warning in the console:

No routes matched location "/dashboard"

Or the component just does not render at all — no error, just a blank area where the route component should appear.

Other related symptoms:

  • The URL changes when you click a <Link>, but the component does not render.
  • Nested routes show the parent but not the child component.
  • All routes render a blank page after upgrading from React Router v5 to v6.
  • The route matches on the root / but not on any other path.
  • A 404 component always renders even when the path exists.

Why This Happens

React Router v6 introduced significant breaking changes from v5. The matching algorithm was rewritten from “first match wins” (v5) to “best match wins” (v6), the <Switch> component was replaced by <Routes>, components are passed via element props instead of component or render, and exact was removed because all routes now match exactly by default. Code that compiles cleanly under v6 can still fail at runtime if any of these conventions are missed — the warning “No routes matched location” is React Router’s way of saying that none of the routes you provided were a good enough match for the current URL.

The matching algorithm looks at every <Route> directly inside a <Routes> element and computes a “score” based on path specificity. Static segments outrank dynamic segments, longer paths outrank shorter paths, and an index route outranks nothing. The route with the highest score wins. A common pitfall is assuming that the order of <Route> declarations affects the result — it does not. If the same path could match two routes equally, v6 throws a development-time warning and picks one deterministically.

Nested routes add another dimension. In v6, a child <Route> only contributes to matching when its parent is matched first. A parent route without /* only matches the parent path exactly — path="/dashboard" matches /dashboard but not /dashboard/settings, even if a child route path="settings" is declared inside. This catches almost every developer migrating from v5 the first time. The fix is either to declare child routes inside the same <Routes> block, or to add /* to the parent path so it matches descendants.

The most common causes of routes not matching:

  • Missing <Routes> wrapper around <Route> elements (v6 requires this).
  • Using <Switch> from v5 — it no longer exists in v6.
  • Using component= or render= props instead of element=.
  • Wrong path syntax — v6 paths are relative by default in nested routes.
  • Missing <Outlet> in parent route components for nested routes to render into.
  • exact prop on <Route> — v6 does not use exact (all routes match exactly by default).
  • Rendering <Route> outside of <Routes> — a <Route> outside <Routes> does nothing in v6.

Platform and Environment Differences

The same route configuration behaves differently depending on where the app is deployed and which host serves the HTML. These are the most common environment-specific traps.

React Router v5 vs v6 vs v7. v5 used <Switch> with exact and component. v6 (the focus of this article) uses <Routes> with element and no exact. v7 (released late 2024) merged with Remix and made the data-router API the default, but the JSX <Routes> API still works. If your npm list react-router-dom shows ^6 or ^7, this article applies. If it shows ^5, the underlying error message and fix are different and you should migrate first or stay on v5 syntax throughout.

Vite, Create React App, and local dev. A dev server like Vite or CRA serves index.html for every unknown route by default, so <BrowserRouter> works seamlessly. Production hosts behave differently. If you deploy to a static host without configuring SPA fallback, navigating directly to /about returns 404 from the host before React Router ever runs. The route never gets a chance to match because the JavaScript bundle is never delivered.

Vercel and Netlify SPA fallback. Both platforms need explicit SPA rewrites. On Vercel, add { "rewrites": [{ "source": "/(.*)", "destination": "/" }] } to vercel.json. On Netlify, add /* /index.html 200 to public/_redirects. Without these, <BrowserRouter> routes 404 on hard refresh but work when navigated via <Link>. The “No routes matched” warning never appears in this case — instead you get a host-level 404.

Cloudflare Pages. Add a _redirects file in your build output with /* /index.html 200. Pages applies this before serving the static asset, so direct navigation to any route returns index.html with a 200 status and React Router takes over.

Subpath deployments (basename). If you deploy under /app/ instead of the root, you must pass basename="/app" to <BrowserRouter>. Otherwise the router strips no prefix and tries to match /app/dashboard against routes like path="/dashboard" — which fail. Vite needs the matching base: '/app/' in vite.config.js, and CRA needs homepage: "/app" in package.json. Mismatched values produce the “No routes matched” warning for every URL.

Electron with file:// URLs. When you package a React app inside Electron and load it from file://, <BrowserRouter> is the wrong choice — the URL bar shows paths like file:///C:/app/index.html#/dashboard, and the history API operates on the file path. Use <HashRouter> so routing uses the # fragment, which works on any URL scheme including file://. The same applies to apps loaded from local filesystem paths in WebView2 or Tauri.

Cordova, Capacitor, React Native WebView. On Cordova and Capacitor, the app loads from capacitor://localhost or ionic://localhost, which can confuse <BrowserRouter> if your routes assume /-rooted paths. <HashRouter> works everywhere without configuration. React Native (without WebView) does not use React Router at all — use @react-navigation/native instead.

<HashRouter> vs <BrowserRouter> trade-offs. Hash URLs (/#/dashboard) survive any host because everything after # is never sent to the server. They are ugly and bad for SEO. Browser URLs are clean but require the host to serve index.html for unknown paths. For internal tools, embedded webviews, or static-only hosts that you cannot configure, <HashRouter> is the right pragmatic choice.

SSR (Next.js, Remix, Astro). Next.js does not use React Router at all — it has a file-system router. If you see “No routes matched” in a Next.js app, you are likely using React Router inside a Next.js page accidentally; remove the import and use Next’s router. Remix uses the React Router data APIs but generates routes from the filesystem too. Astro uses its own routing and only renders React for interactive islands.

Fix 1: Replace Switch with Routes and Update Route Syntax

React Router v6 replaces <Switch> with <Routes> and changes how you pass components:

Broken — v5 syntax in a v6 project:

import { Switch, Route } from "react-router-dom";

function App() {
  return (
    <Switch>
      <Route path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/dashboard" render={() => <Dashboard />} />
    </Switch>
  );
}

Fixed — v6 syntax:

import { Routes, Route } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/dashboard" element={<Dashboard />} />
    </Routes>
  );
}

Key differences:

  • <Switch><Routes>
  • component={Home}element={<Home />}
  • render={() => <Dashboard />}element={<Dashboard />}
  • Remove exact — all routes in v6 match exactly by default.

Fix 2: Wrap All Routes in a Single Routes Component

Every <Route> must be a direct child of <Routes>. You cannot render a <Route> standalone:

Broken — Route outside of Routes:

function App() {
  return (
    <div>
      <Navbar />
      <Route path="/home" element={<Home />} />  {/* Does nothing */}
      <Route path="/about" element={<About />} /> {/* Does nothing */}
    </div>
  );
}

Fixed:

function App() {
  return (
    <div>
      <Navbar />
      <Routes>
        <Route path="/home" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
}

Fix 3: Add Outlet to Parent Components for Nested Routes

Nested routes in v6 require the parent component to render an <Outlet> — a placeholder where the child route renders:

Broken — parent has no Outlet:

// Route config
<Routes>
  <Route path="/dashboard" element={<Dashboard />}>
    <Route path="settings" element={<Settings />} />
    <Route path="profile" element={<Profile />} />
  </Route>
</Routes>

// Dashboard component — missing Outlet
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Settings and Profile never render because there's no Outlet */}
    </div>
  );
}

Fixed — add Outlet:

import { Outlet } from "react-router-dom";

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <nav>
        <Link to="settings">Settings</Link>
        <Link to="profile">Profile</Link>
      </nav>
      <Outlet /> {/* Child routes render here */}
    </div>
  );
}

Pro Tip: Think of <Outlet> as a slot. When the URL matches a child route like /dashboard/settings, React Router renders the parent (Dashboard) with <Outlet> replaced by the child component (Settings). Without <Outlet>, the child has nowhere to render.

Fix 4: Fix Nested Route Paths (Relative vs Absolute)

In React Router v6, nested route paths are relative to their parent by default:

Broken — absolute paths in nested routes:

<Routes>
  <Route path="/dashboard" element={<Dashboard />}>
    <Route path="/dashboard/settings" element={<Settings />} /> {/* Wrong */}
    <Route path="/dashboard/profile" element={<Profile />} />   {/* Wrong */}
  </Route>
</Routes>

Fixed — relative paths:

<Routes>
  <Route path="/dashboard" element={<Dashboard />}>
    <Route path="settings" element={<Settings />} /> {/* Matches /dashboard/settings */}
    <Route path="profile" element={<Profile />} />   {/* Matches /dashboard/profile */}
  </Route>
</Routes>

Render the index route for the parent path itself:

<Routes>
  <Route path="/dashboard" element={<Dashboard />}>
    <Route index element={<DashboardHome />} /> {/* Renders at /dashboard */}
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>

The index prop marks the default child route — it renders when the parent path matches exactly.

<Link> paths in nested components are also relative in v6 unless they start with /:

Broken — missing leading slash in a nested link:

// Inside Dashboard (rendered at /dashboard)
function Dashboard() {
  return (
    <Link to="home">Go Home</Link>  // Navigates to /dashboard/home, not /home
  );
}

Fixed — use absolute paths with leading slash:

function Dashboard() {
  return (
    <Link to="/home">Go Home</Link>  // Navigates to /home
  );
}

Or use relative paths intentionally:

<Link to="../">Up one level</Link>
<Link to="settings">Settings (relative to current route)</Link>

Fix 6: Add a Catch-All 404 Route

If no routes match, React Router renders nothing (an empty <Routes>). Add a wildcard route to render a 404 page:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/dashboard/*" element={<Dashboard />} />
  <Route path="*" element={<NotFound />} /> {/* Catches all unmatched paths */}
</Routes>

The * wildcard matches any unmatched path. Place it last — <Routes> picks the best match, not the first match, but putting * last makes intent clear.

For nested route groups, add /* to the parent:

<Route path="/dashboard/*" element={<Dashboard />}>
  {/* Nested routes work because /* allows matching sub-paths */}
  <Route path="settings" element={<Settings />} />
</Route>

Without /* on the parent, the parent only matches /dashboard exactly and nested paths like /dashboard/settings will not render the parent.

Common Mistake: Forgetting /* on parent routes that have nested children. If your parent route is path="/dashboard" (without /*), it matches /dashboard exactly but not /dashboard/settings. Add /* whenever a route has children.

Fix 7: Fix useNavigate and Programmatic Navigation

React Router v6 replaced useHistory with useNavigate:

Broken — v5 hook in v6:

import { useHistory } from "react-router-dom"; // Does not exist in v6

function LoginPage() {
  const history = useHistory();
  const handleLogin = () => {
    history.push("/dashboard");
  };
}

Fixed:

import { useNavigate } from "react-router-dom";

function LoginPage() {
  const navigate = useNavigate();
  const handleLogin = () => {
    navigate("/dashboard");
    // navigate(-1);          // Go back
    // navigate("/", { replace: true }); // Replace current history entry
  };
}

Fix 8: Ensure BrowserRouter Wraps the App

The entire Router context must wrap your app. If <BrowserRouter> (or <HashRouter>) is missing or placed incorrectly, no routing works at all:

Broken — Routes outside of Router:

// main.jsx
ReactDOM.createRoot(document.getElementById("root")).render(
  <App />  // App contains Routes but no Router wrapping it
);

// App.jsx
function App() {
  return (
    <BrowserRouter>  {/* Router is inside — this is fine */}
      <Routes>...</Routes>
    </BrowserRouter>
  );
}

The above actually works if BrowserRouter is inside App. The error occurs when <Routes> is used with no <Router> ancestor at all:

// Broken — no Router anywhere
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

Fixed — wrap at the entry point:

ReactDOM.createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

For server-side rendering (Next.js), you do not use React Router — Next.js has its own file-based router. For SSR hydration issues, see Fix: Next.js Hydration Failed.

Still Not Working?

Check the installed version. Run npm list react-router-dom to confirm you have v6. If you have v5 installed, either upgrade to v6 or use v5 syntax throughout.

Check for multiple router instances. Having both react-router and react-router-dom installed at different major versions can cause conflicts. Run npm ls react-router to check.

Check for <Route> inside fragments. <Routes> only recognizes direct children. Wrapping <Route> elements in a <React.Fragment> or array may cause them not to be recognized:

// Broken
<Routes>
  <>
    <Route path="/" element={<Home />} />
  </>
</Routes>

// Fixed
<Routes>
  <Route path="/" element={<Home />} />
</Routes>

Check hash vs history routing. If your app is deployed on a static host without server-side routing support, use <HashRouter> instead of <BrowserRouter>. With <BrowserRouter>, navigating directly to /about requires the server to serve index.html for that path. For Cloudflare Pages and similar platforms, add a redirect rule to serve index.html for all routes.

Check the basename in production but not local. If routes work on npm run dev but break on npm run build && serve, the production build sets a different <base href> or you forgot basename on <BrowserRouter>. Inspect the bundled HTML and confirm <base> matches your deployed path. Vite’s base config option, CRA’s homepage, and Next.js’s basePath all affect this differently.

Check for nested <Routes> without /* on the parent. A nested <Routes> inside a component rendered by a route only works if the parent route declared path="/parent/*". Without the /*, the parent only matches /parent exactly and the nested <Routes> never gets a chance to render. Prefer declaring all routes in one <Routes> block using nested <Route> children with <Outlet> — it is faster and easier to debug.

Check for case sensitivity differences. v6 has caseSensitive prop on <Route> (default false). Paths match case-insensitively by default, but URL params do not normalize case — so navigating to /Dashboard matches the route path="/dashboard", but useParams() returns whatever case was in the URL. If your backend is case-sensitive, this mismatch causes lookups to fail. Set caseSensitive to true on routes that should be strict.

Check React 19 + StrictMode interactions. Under React 19 strict mode in development, components mount, unmount, and mount again. A <BrowserRouter> inside <StrictMode> works correctly, but if you wrap routing logic in a useEffect that calls navigate, the double-effect can produce confusing logs. The route itself still matches — it just looks like a phantom navigation. See Fix: React useEffect Runs Twice.

For other React rendering issues like components re-rendering too often, see Fix: React Too Many Re-renders or Fix: React useEffect Infinite Loop.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles