Skip to main content

Route Composition

Composition of <Route> elements in ArchibaldRouter is different from how it worked in React Router v5.

This page explains our rationale for making the change as well as a pattern you will want to avoid in ArchibaldRouter.

Background

In React Router v5, there was 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:

// React Router v5 App
import { Redirect, Route, Switch } 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.

The Problem

The problem is that when you create a wrapper around a <Route> element, 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) {
// ...
}

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 ArchibaldRouter

ArchibaldRouter 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.

In ArchibaldRouter, <Route> is a lot more strict than it was in React Router 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 <Routes>) 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 ArchibaldRouter, you could do something like this:

import { Navigate, Route, Routes } from '@archibald/core';

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>.