Skip to content

Instantly share code, notes, and snippets.

@mjackson
Last active October 30, 2025 21:05
Show Gist options
  • Save mjackson/d54b40a094277b7afdd6b81f51a0393f to your computer and use it in GitHub Desktop.
Save mjackson/d54b40a094277b7afdd6b81f51a0393f to your computer and use it in GitHub Desktop.

Revisions

  1. mjackson revised this gist Oct 15, 2021. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -90,11 +90,11 @@ Not only is the props type declaration messy, but in the majority of cases **you

    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'll throw an error**.
    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.

    ```tsx
    function PrivateRoute(props) {
    // BAD. This will throw in v6!
    // BAD. This code will never run!
    return <Route {...props} />;
    }
    ```
  2. mjackson revised this gist Oct 15, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -49,7 +49,7 @@ If you were using Reach Router, The `<Router>` component worked similarly to v5'

    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 `<ProtectedRoute>` component above, the TypeScript declaration for its props would be an intersection of its own props and those of `<Route>`:
    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>`:

    ```tsx
    interface PrivateRouteProps {
  3. mjackson revised this gist Oct 15, 2021. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -125,7 +125,7 @@ function App() {
    );
    }

    function RequireAuth({ redirectTo, children }) {
    function RequireAuth({ children, redirectTo }) {
    let isAuthenticated = getAuth();
    return isAuthenticated ? children : <Navigate to={redirectTo} />;
    }
    @@ -151,16 +151,16 @@ function App() {
    <Route
    path="/protected"
    render={() => (
    <Private redirectTo="/login">
    <RequireAuth redirectTo="/login">
    <ProtectedPage />
    </Private>
    </RequireAuth>
    )}
    />
    </Switch>
    );
    }

    function Private({ children, redirectTo }) {
    function RequireAuth({ children, redirectTo }) {
    let isAuthenticated = getAuth();
    return isAuthenticated ? children : <Redirect to={redirectTo}>;
    }
  4. mjackson revised this gist Oct 15, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -54,7 +54,7 @@ In the case of our `<ProtectedRoute>` component above, the TypeScript declaratio
    ```tsx
    interface PrivateRouteProps {
    redirectTo: string;
    };
    }

    function PrivateRoute(props: RouteProps & PrivateRouteProps) {
    // ...
  5. mjackson revised this gist Oct 15, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -52,7 +52,7 @@ The problem is that when you create a wrapper around a `<Route>` element, whethe
    In the case of our `<ProtectedRoute>` component above, the TypeScript declaration for its props would be an intersection of its own props and those of `<Route>`:

    ```tsx
    type PrivateRouteProps = {
    interface PrivateRouteProps {
    redirectTo: string;
    };

  6. mjackson revised this gist Oct 15, 2021. 1 changed file with 3 additions and 5 deletions.
    8 changes: 3 additions & 5 deletions composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -39,17 +39,15 @@ function PrivateRoute({ path, children, redirectTo }) {
    }
    ```

    When it came time to render, the `<Switch>` would treat your `<ProtectedRoute>` component the same as a normal `<Route>` element. But did you ever wonder how it worked?
    When it came time to render, the `<Switch>` would treat your `<ProtectedRoute>` component the same as a normal `<Route>` element.

    Here's a short explanation:

    The interesting thing about `<Switch>` in React Router v5 is that, unlike most React components, it 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 `path`s of all its `children` to figure out which ones match the current URL, and then it renders the ones that do.
    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 `path`s 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 the types of props your components may receive.
    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 `<ProtectedRoute>` component above, the TypeScript declaration for its props would be an intersection of its own props and those of `<Route>`:

  7. mjackson revised this gist Oct 14, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -119,7 +119,7 @@ function App() {
    // This is really just inverting the wrapping, but it's a lot
    // more clear which components expect which props.
    <RequireAuth redirectTo="/login">
    <PublicPage />
    <ProtectedPage />
    </RequireAuth>
    }
    />
  8. mjackson revised this gist Oct 14, 2021. 1 changed file with 30 additions and 1 deletion.
    31 changes: 30 additions & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -139,4 +139,33 @@ Notice how in this example the `RequireAuth` component doesn't expect any of `<R

    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.

    When you finally do upgrade to v6, convert `<Route render={() => ...}>` to `<Route element={...}>`.
    To continue with the initial example, you could rewrite your v4/5 code today to look like this:

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

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

    function Private({ 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.
  9. mjackson revised this gist Oct 14, 2021. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -54,11 +54,11 @@ The problem is that when you create a wrapper around a `<Route>` element, whethe
    In the case of our `<ProtectedRoute>` component above, the TypeScript declaration for its props would be an intersection of its own props and those of `<Route>`:

    ```tsx
    type ProtectedRouteProps = {
    type PrivateRouteProps = {
    redirectTo: string;
    };

    function ProtectedRoute(props: RouteProps & ProtectedRouteProps) {
    function PrivateRoute(props: RouteProps & PrivateRouteProps) {
    // ...
    }
    ```
  10. mjackson revised this gist Oct 14, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -51,7 +51,7 @@ If you were using Reach Router, The `<Router>` component worked similarly to v5'

    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 the types of props your components may receive.

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

    ```tsx
    type ProtectedRouteProps = {
  11. mjackson revised this gist Oct 14, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -49,7 +49,7 @@ If you were using Reach Router, The `<Router>` component worked similarly to v5'

    ## 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?) and trying to lock down the types of props that your components may receive.
    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 the types of props your components may receive.

    In the case of our `<ProtectedRoute>` component above, the TypeScript declaration for its props would be a union of its own props, and those of `<Route>`:

  12. mjackson revised this gist Oct 14, 2021. No changes.
  13. mjackson revised this gist Oct 14, 2021. No changes.
  14. mjackson revised this gist Oct 14, 2021. No changes.
  15. mjackson revised this gist Oct 14, 2021. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,8 @@
    # 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](https://reach.tech/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.
    Composition of `<Route>` elements in React Router is changing in v6 from how it worked in v4/5 and in [Reach Router](https://reach.tech/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

  16. mjackson created this gist Oct 14, 2021.
    140 changes: 140 additions & 0 deletions composing-route-in-react-router-v6.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,140 @@
    # 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](https://reach.tech/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](https://github.com/remix-run/react-router/blob/320be7afe44249d5c025659bc00c3276a19f0af9/packages/react-router-dom/examples/Auth.js#L50-L52) to restrict access to certain routes on the page. This element was a simple [wrapper around an actual `<Route>` element](https://github.com/remix-run/react-router/blob/320be7afe44249d5c025659bc00c3276a19f0af9/packages/react-router-dom/examples/Auth.js#L140-L154) 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:

    ```tsx
    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. But did you ever wonder how it worked?

    Here's a short explanation:

    The interesting thing about `<Switch>` in React Router v5 is that, unlike most React components, it 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 `path`s 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?) and trying to lock down the types of props that your components may receive.

    In the case of our `<ProtectedRoute>` component above, the TypeScript declaration for its props would be a union of its own props, and those of `<Route>`:

    ```tsx
    type ProtectedRouteProps = {
    redirectTo: string;
    };

    function ProtectedRoute(props: RouteProps & ProtectedRouteProps) {
    // ...
    }
    ```

    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.

    ```tsx
    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'll throw an error**.

    ```tsx
    function PrivateRoute(props) {
    // BAD. This will throw in v6!
    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:

    ```tsx
    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">
    <PublicPage />
    </RequireAuth>
    }
    />
    </Routes>
    );
    }

    function RequireAuth({ redirectTo, children }) {
    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.

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