• Loading https://cdn.sanity.io/images/fwtcx5qs/production/1671e45d90e74b1f6d9cf79add7f122df7c6b49a-1000x1000.png

Work

React Router Guards

  • Role

    Engineer at Upstatement

  • Dates

    May 2019 – Present

  • What I did

    • Front-End Dev,
    • Visual Design
Building a middleware API for React Router.

Motivation

We were working with Sports Innovation Lab to rebuild their app from a statically-generated site to a dynamic React web app. We decided to use React Router for our routing solution, and Redux as a global store and interface to interact with our numerous APIs. While using this solution, however, we ran into three problems:

  1. One of our APIs required an auth token, that would expire in the middle of a user’s session. This meant Redux actions were still calling this API, and continually failing because a token hadn’t been fetched in time yet.
  2. We had to fetch a lot of the same data for every page, often several endpoints for a single page.
  3. The app called for user roles and restricting page access based on those roles. We also had to do the same for non-authorized users.

We started with the first issue, and decided to write custom Redux middleware that would queue called actions until a token was issued. However, this functionality was useless for actions that didn’t update the store and we’d end up having to write the same conditionals in the component to recall the functions when a token was issued.

We then moved onto the second issue, our solution to which was adding loading, error, and data variables to every component. However, this added a lot of boilerplate and still didn’t seem like the right solution.

Finally, we tackled the third issue. We landed on creating a wrapper component for a Route that would check for auth before allowing access to a page, and otherwise redirecting the user to the login page. We didn’t find any issues with this solution, and wondered if we could abstract it solve our other problems too.

The short answer: we could.

Developing the Library

When I started developing the new solution, I looked at how other frameworks handled this problem. I looked at Vue Router and their concept of Navigation Guards, which exposed a beforeEnter function for every route which could run logic and pass it to the page component before render. I also looked at Express and their app/router middleware, which allowed for a nearly limitless queue of middleware functions to run before reaching the route.

Combining these two implementations, I got to work on building a middleware API for React Router, that would allow us to perform complex logic between the call for navigation and the final render of the route. The solution we built for the client was so well received that my manager pushed me to try to open-source it. That led into a full rewrite that would allow for greater usability in a variety of different projects.

Defining a Guard

I began the rewrite by focusing on how we define what a guard is. In the initial iteration, guards were used to call various APIs to fetch tokens and data, meaning that there needed to be some sort of asynchronous data fetching. A caveat to that, however, was that some guards required data fetched by previous ones, so, despite guards running asynchronous actions, they still needed to be run synchronously one after the other. This is where I developed a prototypal loop that would run through a queue of guard functions. To make this process easier, I took a note from Express’s notebook and passed a Promise’s resolve function (disguised as a function called next) that would alert the loop when a guard had been “resolved.”

With this next function now defined, I decided I wanted to add additional functionality to it rather than just resolving a guard. One such functionality was being able to pass data fetched in a guard as props to a page component; this would serve useful for, say, article pages on a news site that require article data before displaying the page. The other functionality I built in was redirection, allowing developers to redirect the user to a different page based on logic in the guard; I pictured this would be useful in authorization cases.

Built into this loop was also error handling. While a guard was “resolving,” a loading page will be displayed to the user. However, if an error occurs in a guard, such as an unfound resource from an API, we’d want to display an error page without causing a redirect. In order to do so, a developer need only throw an error from their guard, which will then be caught in the loop and be passed as a prop to the error page.

A diagram that illustrates the lifecycle for routing using the library.
The above diagram illustrates the new lifecycle for routing with guards, including the queue loop for resolving or rejecting guards.

Below is an example guard for fetching an article from an API and then passing said article as a prop to the page:

JavaScript
// <Route path="/article/:slug" component={Article} />
const getArticle = async (to, from next) => {
const { slug } = to.match.params;
try {
const article = await api.articles.get(slug);
next.props({ article });
} catch {
throw new Error('404: Article not found');
}
};

A Means of Distrubution

A guard has now been defined, but the way in which we distribute and queue these guards had yet to be solidified yet. In the initial iteration, global guards—those being guards applied to all routes—were defined statically in our Route wrapper component, and individual guards—those being specific to a single route—were passed as a prop to the Route wrapper. Having to abstract this now to fit more than one use case, I decided to create two separate components: the GuardProvider and the GuardedRoute.

The GuardProvider is a high-level wrapper for the entire routing solution. It can accept an array of guards, which, using the Context API, are applied as global guards to all routes. It can also accept global loading and error pages to display while guards are resolving or if they fail, respectively.

The GuardedRoute acts a replacement for the default Route component, accepting all of the same props and providing the same functionality with the added benefit of guard middleware. Like the GuardProvider, it can also accept an array of guards, this time adding to a guard queue specific to this route, as well as specific loading and error pages. It also features a meta object prop, which can provide more context about the route to guard functions.

Presentations and Demos

After months of hard work in-between projects, the library was developing nicely and was almost ready for release. Nearing the end of my apprenticeship, I decided to present a company-wide share on the library I had developed, hoping to open it up to discussion and feedback from my peers. Below are a few slides from that deck:

1/8

The intro slide, with the name "React Router Guards" and temporary logo
A slide reading "Premise: Build a guard middleware API for React Router allowing you to perform complex logic between the call for navigation and the final render of a route. And then open-source it."
A slide outlining what a guard is, with the code example from above.
A slide explaining what a Guard Provider is, with a code example.
A slide explaining was a Guarded Route is, with a code example.
A slide illustrating the distribution of guards from a global to an individual level. The guard "fetchStreamToken" is added to the beginning of three individual guard queues.
A slide illustrating the difference between global and individual loading pages.
A slide illustrating the difference between global and individual error pages.

Following the share, I received tons of feedback and even led a fantastic roundtable to demo the library in people’s existing projects.

With the library tested and ready for release, I decided the best way to show its use was to create a demo. I built a small Pokédex using the PokéAPI, creating a guard that would fetch a Pokémon using a name slug. It featured a shaking Pokéball animation for the loading state, and a MissingNo for error states. You can check out the demo here.

1/4

A list of Pokémon names, including Bulbasaur, Ivysaur, and Venusaur.
A Pokéball rotated in the center of the page
A detailed view for Venusaur, showing its sprites, types, abilities, and statistics.
An image of the MissingNo Pokémon appears above text that reads "Uh-oh! We couldn't catch that Pokémon."

The demo's home page features a list of the Pokémon in order. Every 13th one is replaced with a MissingNo in order to demo the error pages.

On the last day of my Upstatement apprenticeship, I published the library to NPM and opened up our GitHub repo to the public. For anyone out there wondering if they should dive into open-source, do it. This was an invaluable experience for me, opening myself up to new practices and technologies I never used before, and now other people get to benefit from it too! To find out more about the library, check out our GitHub repo or install it today from NPM!