Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Notes on route composition in React Router v6, along with a suggested improvement you can make today to start upgrading

Composing <Route> in React Router v6

Composition of <Route> elements in React Router is changing in v6 from how it worked in v4/5 and in Reach Router. React Router v6 is the successor of both React Router v5 and Reach Router.

This document explains our rationale for making the change as well as a pattern you will want to avoid in v6 and a note on how you can start preparing your v5 app for v6 today.

Background

In React Router v5, we had an example of how you could create a <PrivateRoute> element to restrict access to certain routes on the page. This element was a simple wrapper around an actual <Route> element that made a simple decision: is the user authenticated or not? If so, render the children prop. Otherwise, render a <Redirect> to the login page.

It looked something like this:

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

function App() {
  return (
    <Switch>
      <Route path="/public">
        <PublicPage />
      </Route>
      <PrivateRoute path="/protected" redirectTo="/login">
        <ProtectedPage />
      </PrivateRoute>
    </Switch>
  );
}

function PrivateRoute({ path, children, redirectTo }) {
  let isAuthenticated = getAuth();
  return (
    <Route
      path={path}
      render={() => (
        isAuthenticated ? children : <Redirect to={redirectTo} />
      )}
    />
  );
}

When it came time to render, the <Switch> would treat your <ProtectedRoute> component the same as a normal <Route> element.

This is because <Switch>, unlike most React components, uses the props of its children to decide which ones to render. This is a little non-standard, but children is just a prop after all. So it's not too different from deciding what to render based on any other prop you receive. In the case of <Switch>, it actually looks through the paths of all its children to figure out which ones match the current URL, and then it renders the ones that do.

If you were using Reach Router, The <Router> component worked similarly to v5's <Switch>. Except it took this one step further and eliminated the <Route> component altogether and just used your own custom components instead for convenience.

The Problem

The problem is that when you create a wrapper around a <Route> element, whether it's a v5-style <ProtectedRoute> component or a Reach Router custom component, these components must expect all the props of <Route> in addition to any other props they receive. This becomes particularly painful if you're using TypeScript (or propTypes, remember those?) to declare your component interface.

In the case of our <PrivateRoute> component above, the TypeScript declaration for its props would be an intersection of its own props and those of <Route>:

interface PrivateRouteProps {
  redirectTo: string;
}

function PrivateRoute(props: RouteProps & PrivateRouteProps) {
  // ...
}

The problem was even more apparent when using TypeScript with Reach Router where you didn't have a <Route> component and every one of your custom route components was required to accept all route props as well as its own.

import { Router } from "@reach/router";

function App() {
  return (
    <Router>
      <HomePage path="/" />
      <AboutPage path="/about" />
    </Router>
  );
}

function HomePage(props: RouteProps & HomePageProps) {
  // ...
}

function AboutPage(props: RouteProps & AboutPageProps) {
  // ...
}

Not only is the props type declaration messy, but in the majority of cases your route components are receiving props that they don't actually do anything with. Why? Because these props were meant for <Route>, not them.

<Route> Composition in React Router v6

React Router v6 introduces a new <Routes> element that replaces <Switch>. One of the main advantages of <Routes> over <Switch> is its ability to understand nested <Route> elements, much like we did in React Router v3. We'll write more about just how cool <Routes> is in the official v6 docs.

In v6, <Route> is a lot more strict than it was in v5. Instead of building wrappers for <Route>, it may be used only inside other <Routes> or <Route> elements. If you try to wrap a <Route> in another component like PrivateRoute it will never render. So any custom logic you have in PrivateRoute will never run. If you try to render a <PrivateRoute> as a standalone <Route> (i.e. outside a <Switch>) it will throw an error.

function PrivateRoute(props) {
  // BAD. This code will never run!
  return <Route {...props} />;
}

Instead of creating wrappers for your <Route> elements to get the functionality you need, you should do all your own composition in the <Route element> prop.

Taking the example from above, if you wanted to protect certain routes from non-authenticated users in React Router v6, you could do something like this:

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

function App() {
  return (
    <Routes>
      <Route path="/public" element={<PublicPage />} />
      <Route
        path="/protected"
        element={
          // Good! Do your composition here instead of wrapping <Route>.
          // This is really just inverting the wrapping, but it's a lot
          // more clear which components expect which props.
          <RequireAuth redirectTo="/login">
            <ProtectedPage />
          </RequireAuth>
        }
      />
    </Routes>
  );
}

function RequireAuth({ children, redirectTo }) {
  let isAuthenticated = getAuth();
  return isAuthenticated ? children : <Navigate to={redirectTo} />;
}

Notice how in this example the RequireAuth component doesn't expect any of <Route>'s props. This is because it isn't trying to act like a <Route>. Instead, it's just being rendered inside a <Route>.

Get Started Upgrading Today

If you want to get a head start on upgrading your React Router v5 app to v6 today, you can eliminate any custom route components in your <Switch>es and just use plain <Route>s instead. Then, do your composition inside the <Route render> prop.

To continue with the initial example, you could rewrite your v4/5 code today to look like this:

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

function App() {
  return (
    <Switch>
      <Route path="/public">
        <PublicPage />
      </Route>
      <Route
        path="/protected"
        render={() => (
          <RequireAuth redirectTo="/login">
            <ProtectedPage />
          </RequireAuth>
        )}
      />
    </Switch>
  );
}

function RequireAuth({ children, redirectTo }) {
  let isAuthenticated = getAuth();
  return isAuthenticated ? children : <Redirect to={redirectTo}>;
}

When you finally do upgrade to v6, convert <Route render={() => ...}> to <Route element={...}> and you're done.

@shiraze
Copy link

shiraze commented Nov 18, 2021

What migration path should users take that used <Route render> to inject the location and history props into a class component?
That is, in code like the following:

export default class MainApplication extends Component {
....
            <Route
              render={({ location, history }) => (
                <SearchDrawer
                  location={location}
                  history={history}
                  reload={reload}
                  setReload={this.setReload}
                  openMain={mainDrawer}
                />
              )}
            />
...
}

where Route is being used in a class component to pass some route related props to another class component (SearchDrawer), there doesn't appear to be a migration path. I'm aware of the move from history to navigation, and that's quite trivial, but to assume that the render prop can be replaced with element doesn't seem to fit

@jvnlwn
Copy link

jvnlwn commented Nov 18, 2021

@shiraze The idea comes to mind that you could create a "RouteElement" component that accepts your SearchDrawer component (or any such component) as prop. The RouteElement component can handle passing in the expected route state via hooks.

const RouteElement = ({ component: RouteComponent, ...passProps }) => {
  const location = useLocation()
  const navigate = useNavigate()
  return <RouteComponent {...passProps} location={location} navigate={navigate} />
}

<Route
  element={
    <RouteElement
      component={SearchDrawer}
      reload={reload}
      setReload={this.setReload}
      openMain={mainDrawer}
    />
  }
/>

@sorinpav
Copy link

sorinpav commented Nov 18, 2021

@jvnlwn We were thinking about the same sort of workaround, but we wanted to know if there was an official answer towards this. (I'm part of @shiraze's team)

Are you part of the React Router team, and is this an official answer? If it's not, we'll keep this open so that we can hopefully get something more official.

@jvnlwn
Copy link

jvnlwn commented Nov 18, 2021

@sorinpav ah no, I am not part of the React Router team. Definitely not an official answer. Happened to be looking over the v6 upgrade docs and rabbit-holed here.

@sorinpav
Copy link

sorinpav commented Nov 18, 2021

Thank you so much! Help is indeed greatly appreciated! :) However, we're still hoping we could get something more official. Let's just wait and see...

@shiraze
Copy link

shiraze commented Nov 18, 2021

Thanks @jvnlwn I would have liked to 👍 "thumbs up" your suggestion, rather than spamming the thread, but I couldn't find any such option 😄

@jvnlwn
Copy link

jvnlwn commented Nov 18, 2021

@sorinpav @shiraze you're welcome! Hope you get an official answer. I'd imagine there are others in the same boat.

@alirezamirian
Copy link

alirezamirian commented Nov 19, 2021

Thanks for the very nice explanation. Creating PrivateRoute wrapper around Route vs the suggested approach here seems to me like the "inheritance vs composition" debate in OOP.

@ses-education
Copy link

ses-education commented Nov 22, 2021

That's nice and clean, but what to do with props.match within element?
I've replaced the render prop with element={<Component/>}, but the component doesn't receive props.match at all, it only receives props I explicitly feed to it.
I'm using classes, not functional components. how do I get the props.match data within the component?

UPD: figured it out!
Can pass a wrapper HOC as element and inject params from within it to the rendered component using useParams hook.
If anybody needs an example solution - here's a sandbox:
https://codesandbox.io/s/react-router-v5-to-v6-compatibility-tlhb4

@valentinbourqui
Copy link

valentinbourqui commented Nov 22, 2021

Here you say "When you finally do upgrade to v6, convert <Route render={() => ...}> to and you're done." and inside the documentation for upgrading v5 to v6 you say : "Use instead of and/or props" . So what is the right way to upgrade this part ?

@danBamikiya
Copy link

danBamikiya commented Nov 25, 2021

Thanks for the very nice explanation. Creating PrivateRoute wrapper around Route vs the suggested approach here seems to me like the "inheritance vs composition" debate in OOP.

exactly

@devMarilia
Copy link

devMarilia commented Dec 1, 2021

**
Please help me

When the Route is not inside it gives an error:
Error: A is only ever to be used as the child of element, never rendered directly. Please wrap your in a

When it is inside it give an error as:
Error: [Abc] is not a component. All component children of must be a or <React.Fragment>

Pls help me to resolve this situation ? Or any suggestion.

Tried this but one of the above error in both cases

As this does not generate an error, however my child component of the private route does not render;

import React from 'react'
import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom';


import Header from './components/Header'
import Home from './components/Home'
import Login from './components/Login'
import ProtectedRoute from './components/Helper/ProtectedRoute';
import { UserStorage } from './UserContext';
import User from './components/User';



const App = () => {
  

  return (
    <div>
      <BrowserRouter>
        <UserStorage>
         <>
         <Header />
          <Routes>
            <Route path="/" element={<Home />} />
               <Route path="login/*" element={<Login />} />
            
               <Route  path='/conta' element={<ProtectedRoute/>}>
                  <Route path='/conta' element={<User/>}/>
               </Route>
          </Routes>
         </>
        </UserStorage>
      </BrowserRouter>
    </div>
  )
}

export default App


import React from 'react';
import { UserContext } from '../../UserContext';
import {Routes, Route,  Navigate } from 'react-router-dom';

const ProtectedRoute = (props) => {
  const { login } = React.useContext(UserContext);

  if (login === true) return (
    <Routes>
      <Route {...props} />
    </Routes>
  );
  else if (login === false) return <Navigate to="/login" />;
  else return null;
};

export default ProtectedRoute;

**

@danBamikiya
Copy link

danBamikiya commented Dec 1, 2021

@devMarilia I believe the <User /> is supposed to be a child of <ProtectedRoute /> as in:

const App = () => {
  

  return (
    <div>
      <BrowserRouter>
        <UserStorage>
         <>
         <Header />
          <Routes>
            <Route path="/" element={<Home />} />
               <Route path="login/*" element={<Login />} />
            
               <Route path='/conta' element={ <ProtectedRoute> <User /> </ProtectedRoute> }>
          </Routes>
         </>
        </UserStorage>
      </BrowserRouter>
    </div>
  )
}

export default App


import React from 'react';
import { UserContext } from '../../UserContext';
import {Routes, Route,  Navigate } from 'react-router-dom';

const ProtectedRoute = ({ children }) => {
  const { login } = React.useContext(UserContext);

  if (login === true) return (
    children
  );
  else if (login === false) return <Navigate to="/login" />;
  else return null;
};

export default ProtectedRoute;

can you try this?

@JacobKGilbert
Copy link

JacobKGilbert commented Dec 7, 2021

I'm not sure that what I'm about to ask is possible in v6, but would it be possible to incorporate some kind of middleware handling into the route? Similar to the way it works in Node.js/Express.

router.get("/:id", ensureAdmin, async function (req, res, next)...

I guess I'm asking if something like the following is even possible
<Route path="/private" middleware={authUser} element={<PrivatePage />} />

If it passes the authUser middleware then it would proceed as usual. If not, maybe have the <Navigate> be returned by the authUser.

@DantesSagan
Copy link

DantesSagan commented Dec 7, 2021

@danBamikiya
A lot of thanks, you example work in my project really well!
Was reading migration, but in documentation I've not found examples how to work with it.

import { lazy, Suspense } from 'react';
import UserContext from './context/user';
import useAuthListener from './hooks/use-auth-listener';

import * as ROUTES from '../src/constants/routes';
import ProtectedRoute from './helpers/protected-route';

import Loader from './fallback/loader';

import './App.css';

const Login = lazy(() => import('./pages/login'));
const Dashboard = lazy(() => import('./pages/dashboard'));
const SignUp = lazy(() => import('./pages/signUp'));
const NotFound = lazy(() => import('./pages/not-found'));
const Profile = lazy(() => import('./Components/profile/index'));
export default function App() {
  const { user } = useAuthListener();
  return (
    <UserContext.Provider value={{ user }}>
      <BrowserRouter>
        <Suspense fallback={<Loader />}>
          <Routes>
            <Route path={ROUTES.LOGIN} element={<Login />} />
            <Route path={ROUTES.SIGN_UP} element={<SignUp />} />
            <Route path={ROUTES.PROFILE} element={<Profile />} />
            <Route
              path={ROUTES.DASHBOARD}
              element={
                <ProtectedRoute user={user}>
                  <Dashboard />
                </ProtectedRoute>
              }
            />
            <Route path={ROUTES.DASHBOARD} element={<Dashboard />} />
            <Route path={ROUTES.NOT_FOUND} element={<NotFound />} />
          </Routes>
        </Suspense>
      </BrowserRouter>
    </UserContext.Provider>
  );
}

and

import { Navigate } from 'react-router';
import PropTypes from 'prop-types';

import * as ROUTES from '../constants/routes';

export default function ProtectedRoute({ user, children }) {
  if (user) {
    return children;
  }
  if (!user) {
    return <Navigate to={ROUTES.LOGIN} />;
  }
  return null;
  // <Route
  //   {...rest}
  // render={({ location }) => {}}
  // />
}

ProtectedRoute.propTypes = {
  user: PropTypes.object,
  children: PropTypes.object.isRequired,
};

@mariotacke
Copy link

mariotacke commented Dec 8, 2021

Similar to @JacobKGilbert 's question: do we have to wrap each individual sub-route with <ProtectedRoute>? Ideally I'm looking to define a <RequireAuth> wrapper and have sub-routes adhere to it.

What I am doing now:

function App() {
  return (
    <div className="App">
      <Router>
        <Routes>
          <Route path="/" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/reset" element={<Reset />} />
          <Route path="*" element={
            <RequireAuth>
              <Routes>
                <Route path="/dashboard" element={<Dashboard />} />
                <Route path="/other" element={<Other />} />
              </Routes>
            </RequireAuth>}
          />
        </Routes>
      </Router>
    </div>
  );
}
export function RequireAuth ({ children }) {
  const [user, loading] = useAuthState(auth);
  const navigate = useNavigate();

  if (loading) {
    return null;
  }

  if (!user) {
    return navigate('/', { replace: true });
  }

  return children;
}

This works, but requires me to define a path="*" catch-all. I suppose mapping an array of routes and wrapping them individually works as well. Is there a better way to do this?

@Feng-JY
Copy link

Feng-JY commented Dec 11, 2021

This works

Home page

<div className="app">
      <AppHeader></AppHeader>
      <div className="app-content">
             <AppMenu></AppMenu>
             <div><Outlet /><div>
       </div>
</div>

Routes

 <Router>
      <AuthProvier>
        <Routes>
          <Route path="login" element={<Login />}></Route>
          <Route path="home" element={<RequireAuth><Home /></RequireAuth>}>
            <Route path="user" element={<User />} />
            <Route path="system" element={<System />} />
            <Route path="customer" element={<Customer />} />
            <Route path="customer/:id" element={<CustomerDetail />} />
          </Route>
          <Route path="*" element={<Navigate to="/home" />}></Route>
        </Routes>
      </AuthProvier>
  </Router>

@codeaid
Copy link

codeaid commented Jan 11, 2022

The whole element and/or children approach (what is even the difference between them two?) is actually rather annoying. I never have all routes in one place declared as components, as in:

<Switch>
  <Route ... />
  <Route ... />
  <Route ... />
</Switch>

I always have them as an array of various configurations objects where I can enable or disable authentication, layouts or any other thing that a specific route requires, kind of like this:

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

type CustomRouteProps = RouteProps & { my own stuff }

const routes: Array<CustomRouteProps> [
  { route 1 },
  { route 2 },
  ...
  { route N },
]

and each route would have its own component property:

{
  component: lazy(
    () => import(/* webpackChunkName: 'my-dog-page' */ 'pages/MyDogPage'),
  ),
}

Now I apparently can't even pass in a component but have to pass in a rendered element so I end up having to convert my file from ts to tsx, declaring every single page at the top of the file like this:

const MyDogPage = lazy(
  () => import(/* webpackChunkName: 'my-dog-page' */ 'pages/MyDogPage'),
);

and then having to render each of them inside the config entry:

{
  element: <MyDogPage />
}

or is it

{
  children: <MyDogPage />
}

?

Why not keep the component property and just check if it exists and use it, otherwise fall back to element? As it stands this is a rather massive change that causes a lot of inconvenience when all of this could've been avoided.

@Rajesh-Royal
Copy link

Rajesh-Royal commented Jan 18, 2022

@mjackson Nice guide.
Also, there is a minor spelling mistake in the name of "React Router" it is written as "Reach Router".

image

@kor-developer
Copy link

kor-developer commented Jan 25, 2022

@Rajesh-Royal

I believe the document has been written as intended, because
there actullay IS a Reach Router.
Check it out!

@aderchox
Copy link

aderchox commented Feb 9, 2022

Really helpful gist! It's really the "gist" of it! 😊👌

@david-lee
Copy link

david-lee commented Feb 23, 2022

Anyone who tried to make it work in Electron environment? My electron app is working fine with V5 but when I upgraded to v6, I got this error.
All component children of <Routes> must be a <Route> or <React.Fragment>
I made a very simple Routes but it didn't work. My Electron version is 13.3.0. Any ideas about the error?

<Router> // <-- HashRouter
  <Routes>
    <Route path="/" element={<Home />}  />
  </Routes>
</Router>

I am not sure if HashRouter has something to do with the error.

@Folasayo-Samuel
Copy link

Folasayo-Samuel commented Mar 12, 2022

@david-lee
Try this:

import {BrowserRouter as Router, Routes, Route} from "react-router-dom"

<Router>

 <Routes> 

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

    <Route path="users/*" element={<Users />} /> 

  </Routes> 
</Router> 

This should work. Just make it simple

@caio2525
Copy link

caio2525 commented Mar 20, 2022

function RequireAuth({ children, redirectTo }) {
  let isAuthenticated = getAuth();
  return isAuthenticated ? children : <Navigate to={redirectTo} />;
}ge": 25
}

Is it possible to make RequireAuth an async function, so that we could call await getAuth()?

@Kais3rP
Copy link

Kais3rP commented Mar 27, 2022

@mjackson I think you meant to call the component PrivateRoute and not ProtectedRoute and there's a missing slash on last Redirect:
https://gist.github.com/Kais3rP/93898c9403aca035103b225cdbb964fc#file-composing-route-in-react-router-v6-md .

@followben
Copy link

followben commented Apr 15, 2022

@caio2525 I needed async for getAuth too. This is what I came up with:

export function RequireAuth({ children, redirectTo }) {
  const auth = useAuth();
  const [tryAuth, setTryAuth] = useState(!auth.token);

  useEffect(() => {
    (async () => {
      if (!auth.token) {
        await auth.getAuth(); // populates auth.token on success
        setTryAuth(false);
      }
    })();
  }, []);

  if (tryAuth) return <p>Checking...</p>;
  return auth.token ? children : <Navigate to={redirectTo} />;
}

@caio2525
Copy link

caio2525 commented Apr 16, 2022

@followben Nice. I shall give it a try. thanks.

@afobaje
Copy link

afobaje commented Apr 24, 2022

Please how do you use context with react router v6, couldn't find a proper documentation on this and useOutlet context returns an error

@norayr93
Copy link

norayr93 commented May 4, 2022

@codeaid I am encountering the same issue, any workaround?

@afobaje
Copy link

afobaje commented May 5, 2022

Found my work around, you can actually use react context with v6 by importing the createContext and initiating create context with a variable. P.S. make sure you are exporting the variable. Wrap around this context in your parent app or a component common to other components as a provider. e.g export let mycontext=createContext();
<mycontext.Provider value={{'your value'}}>
//your parent app here
<mycontext.Provider>
then initiate in subcomponent by importing usecontext hook
let values=useContext(mycontext)
console.log(values).
I hope it helps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment