4

As stated in React Router 6's documentation, the simples way to add a global 404 page is to use a wildcard * route i.e.:

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

But, how should we go in a more complex scenario with several descendant routes with their own layout and route definitions like this:

function Feature1 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
      </Routes>
    </FeatureLayout>
  );
}

function Feature2 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
      </Routes>
    </FeatureLayout>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="feature1/*" element={<Feature1 />} />
      <Route path="feature2/*" element={<Feature2 />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

In this case, our global * route is not going to be reached if we fall under the feature1/*, and feature2/* descendant routes, because the routing will go on and be handled by the underlying components. Descendant routes can go several levels deep because its a really convenient way to organize routes.

So my question is, how to have a single standard 404 page with its own layout that gets fired if no routes or descendant routes match the current path?

Important: Please do not suggest explicit /404 page, the solution should keep the URL without redirecting to /404 path.

3 Answers3

3

Create a single 404 not-found route and redirect to it from any nested Routes component.

Example:

Feature1

function Feature1 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
        <Route path="*" element={<Navigate to="/404" replace />} /> 
      </Routes>
    </FeatureLayout>
  );
}

Feature2

function Feature2 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
        <Route path="*" element={<Navigate to="/404" replace />} />
      </Routes>
    </FeatureLayout>
  );
}

App

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="feature1/*" element={<Feature1 />} />
      <Route path="feature2/*" element={<Feature2 />} />
      <Route path="/404" element={<NotFound />} />
      <Route path="*" element={<Navigate to="/404" replace />} />
    </Routes>
  );
}

Update

If you don't want a single, explicit "404" route, then the alternative is to render one for each set of routes you are rendering.

Example:

Feature1

function Feature1 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </FeatureLayout>
  );
}

Feature2

function Feature2 () {
  return (
    <FeatureLayout>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </FeatureLayout>
  );
}

App

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="feature1/*" element={<Feature1 />} />
      <Route path="feature2/*" element={<Feature2 />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Update 2

Reconfigure the layouts so they are only rendering the routes you want within them. Render the NoutFound component in a route outside any specific layout route.

Example:

Ensure the layout components render an Outlet for nested routes to render their content into.

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

const FeatureLayout = () => {
  ...

  return (
    <>
      ... layout UI
      <Outlet /> // <-- nested routes render here
    </>
  );
};

Feature1

function Feature1 () {
  return (
    <Routes>
      <Route element={<FeatureLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Feature2

function Feature2 () {
  return (
    <Routes>
      <Route element={<FeatureLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="foo" element={<Foo />} />
        <Route path="bar" element={<Bar />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

App

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="feature1/*" element={<Feature1 />} />
      <Route path="feature2/*" element={<Feature2 />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Yeah, I thought about this as well, but I am looking for solution that will persist the the URL which is a more appropriate solution in terms of UX and overall I have not came across this explicit /404 page solution anywhere. – Sanjar Mirakhmedov Jun 07 '22 at 08:17
  • @SanjarMirakhmedov Can you clarify what you mean by "persist the the URL which is a more appropriate solution in terms of UX"? It's not uncommon to have a single specific route to handle unknown routes. The alternative is to render a `} />` with each set of routes. – Drew Reese Jun 07 '22 at 08:19
  • Indeed it is uncommon, I don't see any large companies using this approach, or if you know please give some examples, personally it feels like a hack, because you don't need extra page if you can handle 404 on the same path, its just a tech limitation imho. Yeah indeed we can have `}/>` with each set of routes, but what if those Routes are surrounded by layout component, and ideally 404 page should not be rendered between the layout of other module, and be standalone and look the same for any url, regardless of where it is being rendered. – Sanjar Mirakhmedov Jun 07 '22 at 08:27
  • @SanjarMirakhmedov That's what the redirect is for, to get "out of" any nested layouts. OFC, you can also reconfigure the nested route components so they don't render the `NotFound` component within any specific layout. – Drew Reese Jun 07 '22 at 08:30
  • 1
    @SanjarMirakhmedov I've added another update with a suggested refactor to render the multiple `NotFound` components, all outside any layout components. – Drew Reese Jun 07 '22 at 08:37
0

There is not automated way, just copy this

<Route path="*" element={<Navigate to="/404" replace />} />

At the end of all your nested routes

Ingenious_Hans
  • 724
  • 5
  • 16
-1

According to this answer the order of precedence has changed in v6 and you can simply put your catch-all route at the top. I tested this just now on a local app and it works even with nested routes:

function App() {
  return (
    <Routes>
      <Route path="*" element={<NotFound />} />
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="feature1/*" element={<Feature1 />} />
      <Route path="feature2/*" element={<Feature2 />} />
    </Routes>
  );
}

And you are absolutely correct to want to avoid redirecting to /404 - that is one of the most frustrating patterns on the web.

omid
  • 1,146
  • 1
  • 7
  • 17
  • Unfortunately this solution fails as well, because Router sets weights/priority to routes now, and it will skip the `*` and go further until it again gets trapped in the descendant route. :( – Sanjar Mirakhmedov Jun 07 '22 at 08:28
  • `react-router-dom@6` uses route ranking, so the order is irrelevant. – Drew Reese Jun 07 '22 at 08:31