Route Less Traveled: Scaling with React Router

Resilient Tech
5 min readFeb 4, 2021

by Katie Agresta and Alexander Virga, Software Engineering @ Resilia

React Router is the defacto library to reach for when setting up routing in React. And for good reason! It’s lightweight, and with a seemingly endless number of tutorials available, you can

implement a dynamic, client-side routing layer in your app in minutes. When we first started our React App, we structured our router in a way that would help us get up and running quickly. With fewer than 10 routes, this worked and allowed us to focus on feature development. But as our engineering team and the complexity of our app began to grow, we noticed a few things that had us rethink our strategy.

The Problem: One Array to Rule Them All

If you’ve ever worked with React, chances are you have come across the following route set up — one file that exports a Routes array which contains a config object for each route:

{
path: ‘/about’,

component: AboutContainer,

exact: true,

}

The App.js file maps over the routes array and passes in the config object as props for react-router’s <Route /> component. Sounds familiar, right?

As we planned to scale our app, we realized this setup had the potential to get out of hand quickly.

  • How will we handle authentication and authorization based on user types?
  • How should we structure our nested routes that are 2, 3, or even 4 layers deep?
  • What about error handling and redirects?

In addition to these architectural concerns, we also noticed that our routing implementation was impacting developer experience. Having one array of routes does separate concerns in that all the routes are kept in one place. With this setup we would create the component, then head over to a different part of the app, scroll endlessly through our rapidly growing routes array, and finally add the config object. If you’re touching a route during development, it’s likely because you’re creating one, along with the container component that the route is configured to render. When it came to our development process, it wasn’t separating the right concerns.

The Solution: Let the Components Handle It

“If you’re touching a route during development, it’s likely because you’re creating one, along with the container component that the route is configured to render.”

In short, it might make sense for components to keep track of their own routes. Let’s take a look at this architecture as a whole, then break down each section in detail.

File Structure

The “Pages” directory (left) contains a folder for each page of our application.

Each page contains an index file at its root. These index files are responsible for all routes associated with that page. The diagram below displays a page with and without subroutes.

Similar to the individual pages, the index file at the root of the “Pages” folder gathers the route configs for each page. We now have all of our routes stored in a manageable array.

Now that we have our routes configured, we can move on to error handling and authentication.

Invalid Route / 404 Error Handling

Our app does away with 404 errors. Instead, invalid routes are redirected to the root path (‘/’) of our application.

As we map through our routes, we use a switch statement to check for a matching route. If no matching route is found, the switch will render the root component. If there’s a match, the route is passed to <RoutesWithSubRoutes> for subroute and authorization checks.

Authorization

Before we render our routes, we need to check if users have access to the component. If the user’s access type doesn’t match, the component won’t render.

Note: withAuthenticationRequired() is a method provided by Auth0.

Subroutes

We first need to check if a route config contains subroutes. If so, we render <ComponentWithAuth> and pass the subroutes through as props.

After this step, we no longer need to navigate beyond our component folder in order to handle its routing.

Now, when a component has subroutes, we can accept the subroutes as props, then map over them within the page’s main container.

We are in a stage of rapid development, so new routes and components are being added frequently. One downside is we don’t have a single place to look to see all of our routes, but a strong component to path naming convention mitigates namespace collisions. Overall, since we’ve adopted this pattern we’ve found that our developer experience has vastly improved.

--

--

Resilient Tech

Resilia’s mission is to strengthen the capacity of nonprofits and help grantors scale impact through data-driven technology solutions. https://www.resilia.com