How to implement a standalone 404 page in React Router 6 which is not tied to any parent context when using descendant routes?

Viewed 173

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 Answers

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

There is not automated way, just copy this

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

At the end of all your nested routes

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.

Related