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.

When the Router Stops Matching Anything

Personally, I think React Router v6 has one of the most confusing breaking-change stories in the React ecosystem. Code from v5 tutorials does not compile cleanly in v6, but it ALSO does not produce a useful error; it just silently renders nothing. I learned to check the v6 syntax first and the Outlet placement second whenever a route stops matching. 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.

Quick Reference Before You Dive In

If you arrived here from Google with a fresh routing failure, the five facts that resolve roughly 90 percent of cases:

  1. v6 uses <Routes>, element={<X />}, no exact, no <Switch>. Code copy-pasted from v5 tutorials does NOT work. The React Router v6 upgrading guide and the <Routes> reference are the canonical sources.
  2. Every <Route> MUST be a direct child of <Routes>. A standalone <Route> outside <Routes> silently does nothing.
  3. Nested routes need <Outlet> in the parent component. Without it, child routes have no slot to render into.
  4. For nested routes, add /* to the parent path so it matches descendants. path="/dashboard" matches /dashboard only; path="/dashboard/*" matches /dashboard/anything.
  5. For static hosts (Vercel, Netlify, Cloudflare Pages), configure SPA fallback. Without it, hard refresh on any non-root path returns a host 404 before React Router runs.

The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.

Why v6 Routes Stop Matching

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.

When to Use Which Fix

The next eight sections cover the fixes in detail. The table below maps your situation to the recommended fix.

Your situationRecommended fixWhy
v5 syntax in v6 projectFix 1: replace <Switch> with <Routes>, use elementAPI rewrite
<Route> outside <Routes>Fix 2: wrap routes in <Routes>v6 requires this
Nested routes, parent renders but child does notFix 3: add <Outlet> to parentSlot for child
Nested route paths repeat parent prefixFix 4: use RELATIVE paths in childrenv6 paths are relative by default
<Link> navigates to wrong pathFix 5: leading slash for absolute, no slash for relativeLink paths also relative
Unmatched paths render blankFix 6: add <Route path="*"> 404No automatic fallback
useHistory no longer existsFix 7: useNavigate insteadAPI renamed
<Routes> outside <Router>Fix 8: wrap entire app in <BrowserRouter>Context required

If multiple rows apply, pick the topmost one.

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>
  );
}

The mental model I use for <Outlet>: it is a slot, like a placeholder in a template. When the URL matches a child route, React Router renders the parent component with <Outlet> swapped out for the matched child. Without <Outlet> somewhere in the parent’s JSX, the child has nowhere to render and you get the silent blank-page failure.

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.

A specific mistake I have made more than once during v6 migrations: forgetting /* on parent routes that have nested children. path="/dashboard" matches /dashboard exactly, but never /dashboard/settings (the nested child never has a chance to match). path="/dashboard/*" matches both. I now make adding /* part of my muscle memory when declaring parent routes.

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.

Stranger Causes I Have Tracked Down

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; 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.

What Other Tutorials Get Wrong About React Router

Most React Router tutorials list the same fixes but frame them in ways that produce subtle bugs.

They mix v5 and v6 syntax silently. Copy-pasted examples from v5 era articles use <Switch>, component, and exact. None of these work in v6. Articles that show v5 code without flagging the version produce broken routing.

They miss <Outlet> for nested routes. Nested routes without <Outlet> in the parent silently fail. Articles that show nested <Route> declarations without mentioning the outlet leave readers staring at a parent with no children visible.

They miss the /* requirement for nested parents. path="/dashboard" matches /dashboard only; nested children never get a chance. path="/dashboard/*" matches /dashboard/anything. Articles that omit /* create routes that “work in v5 but not v6.”

They miss SPA fallback for static hosts. Vercel, Netlify, Cloudflare Pages need explicit redirects. Without them, hard refresh on /about returns a host 404 before React ever runs. Tutorials that focus only on the React side leave readers confused when production breaks.

They miss basename for subpath deployments. If deployed at /app/, <BrowserRouter> needs basename="/app". Articles that show only root-deployed examples produce silent breakage for sub-path projects.

They confuse <BrowserRouter> with <HashRouter>. For Electron, file:// URLs, and static-only hosts, <HashRouter> is the correct choice. Articles that recommend <BrowserRouter> universally produce broken apps in those environments.

Frequently Asked Questions

Why does my route render blank instead of showing the component?

Three common causes. First, you have v5 syntax (<Switch>, component=) in a v6 project. Second, the <Route> is outside a <Routes> wrapper. Third, you have nested routes but the parent component is missing <Outlet>. Check each in order.

What is the difference between <Routes> and <Switch>?

<Switch> (v5) renders the FIRST matching <Route>. <Routes> (v6) renders the BEST matching <Route> based on path specificity. The replacement is not just a rename; the matching algorithm changed.

Do I need to add exact to my routes?

No. In v6, all routes match exactly by default. The exact prop does not exist. Remove it from any v5 code you are migrating.

Why does <Link to="settings"> navigate to /dashboard/settings instead of /settings?

In v6, <Link> paths are RELATIVE by default. Inside a component rendered at /dashboard, to="settings" means /dashboard/settings. Use to="/settings" (leading slash) for absolute paths.

Should I use <BrowserRouter> or <HashRouter>?

<BrowserRouter> for normal web apps deployed to a host that can serve index.html for unknown paths. <HashRouter> for Electron, static-only hosts you cannot configure, embedded webviews, or file:// URLs.

Why does my app work locally but break in production?

Almost always SPA fallback. Local dev servers (Vite, CRA) serve index.html for every unknown route. Production static hosts return 404 unless configured otherwise. Add a redirect rule (_redirects for Netlify / Cloudflare, vercel.json rewrites for Vercel) so /about returns index.html instead of 404.

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