Skip to content

Instantly share code, notes, and snippets.

@mjackson
Last active March 12, 2024 08:39
Show Gist options
  • Star 75 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save mjackson/d54b40a094277b7afdd6b81f51a0393f to your computer and use it in GitHub Desktop.
Save mjackson/d54b40a094277b7afdd6b81f51a0393f to your computer and use it in GitHub Desktop.
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.

@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

@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

@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

@andreujuanc
Copy link

andreujuanc commented Jul 6, 2022

Really sad that you are forcing the users to adopt a specific pattern.
We had this really complex props set, that was working beautifully, and now we have to find a workaround because of this.
Yes, sometimes some components were passed some props that were not for them, but now we have to have a wrapper around every single page in order to do what we used to do with our custom route component.

EDITED: REMOVED SALTINESS.

@SPodjasek
Copy link

SPodjasek commented Jul 12, 2022

This also seems to work and it looks cleaner when you have multiple sub-routes to protect:

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

function App() {
  return (
    <Routes>
      <Route path="/public" element={<PublicPage />} />
      <Route path="/protected" element={<RequireAuth redirectTo="/login" />}>
        <Route index element={<ProtectedPage />} />
        <Route path="details" element={<SecondProtectedPage />} />
      </Route>
    </Routes>
  );
}

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

@humanchimp
Copy link

The suggested approach here seems to me like the "inheritance vs composition" debate in OOP

How so? Wrapping is composition. The advised pattern is just awkward

@pfdgithub
Copy link

pfdgithub commented Aug 9, 2022

@codeaid
Dynamically load modules based on route definitions.
Using the component V5 API, the module will load when the route is matched.
Using the element V6 API, the module is already loaded when the route is declared.

@cpatti97100
Copy link

cpatti97100 commented Aug 26, 2022

thanks @SPodjasek , great gist! I improved it a bit for more flexibility in the way RequireAuth is used, also with a child component and/or roles

const RequireAuth = ({
  children,
  allowedRoles = ['editor', 'admin'],
  redirectTo = '/login',
}) => {
  const user = useSelector(selectUser);

  const authSuccessulComponent = children ?? <Outlet />;

  return user &&
    intersection(user.permissions.role, allowedRoles).length > 0 ? (
    authSuccessulComponent
  ) : (
    <Navigate to={redirectTo} />
  );
};

export default RequireAuth;

@kirankuyate2157
Copy link

in Switch Route Routing time it's working, but now latest new Routes, Route it not working custom route
I have wrapped the navbar page and home page in HomeLayoutHOC
can anyone help me :) how to do this latest version I try but so many things. no result for this

I want 'HomeLayoutHOC " route instead of "Route"

->client\src\App.JSX : 
//HOC
import HomeLayoutHOC from "./HOC/Home.Hoc";
import { Route, Routes } from "react-router-dom";
//Component
import Temp from "./Components/temp";

function App() {
  return (
    <>
      <Routes>
        <HomeLayoutHOC path="/" exact element={Temp} />       // <--- I want this to work! 
       // <Route path="/" element={<Temp />} />    //              <-- this working fine 
      </Routes>
    </>
  );
}

export default App;

result 👇

image

->client\src\index.jsx :

 import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.CSS";
import App from "./App";

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

-> client\src\HOC\Home.Hoc.jsx

import React from "react";
import { Route } from "react-router-dom";

// Layout
import HomeLayout from "../Layout/Home.Layout";

const HomeLayoutHOC = ({ component: Component, ...rest }) => {
  return (
    <>
      <Route
        {...rest}
        component={(props) => (
          <HomeLayout>
            <Component {...props} />
          </HomeLayout>
        )}
      />
    </>
  );
};

export default HomeLayoutHOC;

->client\src\Layout\Home.Layout.jsx

import React from "react";

// Components
import Navbar from "../Components/Navbar";

const HomeLayout = (props) => {
  return (
    <>
      <Navbar />
      <div className="container mx-auto px-4 lg:px-20 ">{props.children}</div>
    </>
  );
};

export default HomeLayout;

please give me the possible suggestion for the latest router dom (Routes, Route)

wrapping/composing

@react-route

@Folasayo-Samuel
Copy link

Folasayo-Samuel commented Oct 11, 2022 via email

@taiseen
Copy link

taiseen commented Nov 9, 2022

@kirankuyate2157

->client\src\App.JSX :

//HOC
import HomeLayoutHOC from "./HOC/Home.Hoc";
import { Route, Routes } from "react-router-dom";
//Component
import Temp from "./Components/temp";

function App() {
  return (
    <>
      <Routes>
        <Route path="/" element={
                                 <HomeLayoutHOC> 
                                      <Temp />  
                                 </HomeLayoutHOC>} /> }
        />
      </Routes>
    </>
  );
}

export default App;

-> client\src\HOC\Home.Hoc.jsx

import React from "react";
import { Route } from "react-router-dom";

// Layout
import HomeLayout from "../Layout/Home.Layout";

const HomeLayoutHOC = ({ children }) => {
  return ( 
        <HomeLayout>
            { children }
        </HomeLayout>
  );
};

export default HomeLayoutHOC;

I have a similar problem to yours,
but my problem patterns are a little bit different...

So, You can try this way... hope its works in your case...

@kirankuyate2157
Copy link

oh I tried that time ES6 method for layout so that y I put it here then after research, I found that this was changed in ES7 with Outlet
and problem solved that time .and thank you! 🙌

@stevezhaoUS
Copy link

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 ?

Exactly! I have the same question, I have seen a lot conflicts information while migrating from v5 to v6.

@Naureen13
Copy link

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 ?

Exactly! I have the same question, I have seen a lot conflicts information while migrating from v5 to v6.

Hii, did you get any solution for this as I am facing the same problem right now.

@mickelindahl
Copy link

mickelindahl commented Feb 12, 2024

Hi, I had a wrapper for Route in which we types path to a sepcific set of url:s that is eligable in our project. This feature was very appriated since it made it difficult to create a route that was not in stored in route types object. So I would suggest that you allowed for wrappers again since with the current setup it is impossible to type path to Routes since it is not possible wrap it and add extra typeing to the input. Do not understand why you opted away from this neat feature. Would have been great if it had been kept since it made our life easier.

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