Cover image
Web Front-end
7 minute read

React Router Tutorial: Redirect Like a Pro

Naive React routing increases risk and maintenance. This tutorial provides a full exploration of routing approaches, achieving an elegant solution that seamlessly fits into any React code base.

React Router is the de facto React page switching and routing solution. React Router was one of the first popular, open-source projects around React back in 2014 and has grown along with React to a prominent place within React’s ecosystem.

In this React Router tutorial, I start with a key concept and explain my choice of routing library. I then detail how to create a simple application with just enough programmatic logic to showcase various routing features. Lastly, I focus on implementing an elegant, secure, and reusable component to achieve a minimally intrusive and low-maintenance routing solution. The resulting routing code comports with React’s coding guidelines and style for a seamless fit within any recent React application.

Getting Started: Declarative Routing Basics

Declarative routing is the coding style used within React and React Router. React’s declarative routes are components and use the same plumbing available in any React application. Since routes are components, they benefit from consistent approaches.

These routes associate web addresses with specific pages and other components, leveraging React’s powerful rendering engine and conditional logic to turn routes on and off programmatically. This conditional routing allows us to implement application logic to ensure our routes are correct and adequately secured.

Of course, any router is only as good as its library. Many developers don’t consider quality of life when choosing a library, but React Router v6 delivers a bevy of powerful features to simplify routing tasks and should be the React routing solution of choice.

What makes React Router the best compared to other routing libraries?

  • It has declarative route definitions (using JSX inside of React components).
  • It is the industry standard.
  • It offers code samples galore and a plethora of online tutorials.
  • It provides modern React code conventions (using hooks and functional components).

Developers who are using the previous version, React Router v5, should know about three key changes to React Router v6:

  • The <Switch> component has been renamed <Routes>.
  • A useRoutes() hook replaces react-router-config for defining routes as plain objects.
  • Every component child of <Routes> must be a <Route>. This can break some previous methods for organizing and composing routes.

The remainder of this article explores various v6-compatible patterns and ends with our ultimate and most elegant route composition. For more about upgrading from v5 to v6, check out the official migration guide.

Time to Set Up a Basic React Application

Every great React tutorial needs a basic chassis to showcase its desired features. We expect that your development system has npm installed. Let’s create a simple React project with Vite—there’s no need to install Vite separately—that provides our base React app structure, a standalone web server, and all necessary dependencies:

npm create vite@latest redirect-app -- --template react-ts

This command creates our basic app using TypeScript.

React Routes Basics

React Router redirects users to pages within the client according to associated web addresses. An application’s routing logic includes general program logic, as well as requests for unknown pages (i.e., redirecting to a 404 page).

Since React generates a single-page application (SPA), these routes simulate old-school web applications with separate physical or file-based routing. React ensures that the end user maintains the illusion of a website and its collection of pages while retaining the benefits of SPAs such as instant page transitions. The React Router library also ensures that the browser history remains accessible and the back button remains functional.

Protect Your React Route

React Routes provide access to specific components with an SPA and thus make information and functionality available to the end user. We want users to access only features authorized by our system’s requirements.

Whereas security is essential in our React client, any secure implementation should provide additional (and arguably primary) security features on the server to protect against unauthorized client malfeasance. Anything can happen, and savvy browser users can debug our application via browser development tools. Safety first.

A prime example includes client-side administrative functions. We want these functions protected with system authentication and authorization plumbing. We should allow only system administrators access to potentially destructive system behaviors.

The Easy Solution You Shouldn’t Choose

There is a broad spectrum of expertise within the React developer community. Many novice React developers tend to follow less elegant coding styles regarding routes and associated secure access logic.

Typical naive implementation attributes include:

  • Defining route protection on every page.
  • Relying on useEffect React hooks to accomplish page redirection where unauthorized page access is detected.
  • Requiring an entire page to load before redirect and route protection logic executes.

A naive routing component implementation might look like this:

import { useContext, useEffect } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { UserContext } from '../UserContext'

export default function NaiveApproach() {
  const { loggedIn } = useContext(UserContext)
  const navigate = useNavigate()

  useEffect(() => {
    // Check if the user is logged in (after the page loads)
    // If they're not, redirect them to the homepage
    if (!loggedIn) navigate('/access-denied')
  })

  return (
    <div>Page content...</div>
  )
}

An application would use this routing component like this:

export default function App() {
  return (
    <Router>
      <Routes>
        {/* Method 1: Using `useEffect()` as a redirect */}
        <Route path="/naive-approach" element={<NaiveApproach />} />
      </Routes>
    </Router>
  )
}

This approach is often implemented but should be avoided, as it wastes system performance and annoys our user base. Naive routing will do three things:

  1. Negatively impact our app’s performance.
    • Other useEffect hooks could potentially run before the redirect happens.
    • We could see a system slowdown caused by unnecessary server-side requests. A 75% or more degradation would be unsurprising depending on the number of logic blocks encountered before running security checks.
  2. Potentially cause the site or page to flicker.
    • Because the protected page loads first, it briefly navigates to the requested web address but may redirect, depending on page security logic.
  3. Copy secure routing logic everywhere.
    • This routing logic implementation on every protected page in our application would cause a maintenance nightmare.

Better React Routing With a Custom Component

We want to make our secure routing more elegant. Three things that will help us achieve a better implementation are minimizing code maintenance, centralizing secure routing logic to minimize code impact, and improving application performance. We implement a custom ProtectedRoute component to achieve these goals:

import { ReactNode } from 'react'
import { Navigate } from 'react-router-dom'

/**
 * Only allows navigation to a route if a condition is met.
 * Otherwise, it redirects to a different specified route.
 */
export default function ConditionalRoute({
  condition,
  redirectTo,
  children,
}: ConditionalRouteProps): JSX.Element {
  return condition ? <>{children}</> : <Navigate to={redirectTo} replace />
}

export type ConditionalRouteProps = {
  /**
   * Route is created if its condition is true.
   * For example, `condition={isLoggedIn}` or `condition={isAdmin}`
   */
  condition: boolean

  /** The route to redirect to if `condition` is false */
  redirectTo: string

  children?: ReactNode
}

Our application code requires adjustment to make use of the new ConditionalRoute component:

export default function App() {
  return (
    <Router>
      <Routes>
       {/* Method 2: Using ConditionalRoute (better, but verbose) */}
        <Route
          path="/custom-component"
          element={
            <ConditionalRoute condition={isLoggedIn} redirectTo=”/”>
              <CustomComponentPage />
            </ConditionalRoute>
          }
        />
      </Routes>
    </Router>
  )
}

This implementation is markedly better than the easy, naive solution laid out earlier because it:

  • Achieves secure routing implementation in one component. This compartmentalized implementation significantly improves our code base maintenance cost.
  • Averts unnecessary and unauthorized page routes. This highly focused page routing logic potentially avoids unnecessary server calls and page rendering logic.

Although this implementation is better than others, it is far from perfect. The usage style seen in our application code sample tends to carry more code bloat than we like and is our motivation to write an even more elegant solution.

The Best React Router Solution

We want a truly epic and higher-order implementation that reaches the pinnacle of highly componentized route security, nimble parameter usage, and minimal impact on pages requiring routing. We introduce our elegantly written and lowest-impact component, the GrandFinaleRoute:

/** A higher-order component with conditional routing logic */
export function withCondition(
  Component: FunctionComponent,
  condition: boolean,
  redirectTo: string
) {
  return function InnerComponent(props: any) {
    return condition ? <Component {...props} /> : <Navigate to={redirectTo} replace />
  }
}

/** A more specific variation */
export const withLoggedIn = (Component: React.FunctionComponent) =>
  withCondition(Component, useContext(UserContext).loggedIn, '/home')

This secure routing component not only meets all of our requirements, but also allows for an elegant and concise usage without our page components:

const GrandFinaleRoute = withLoggedIn(HigherOrderComponentPage)

export default function App() {
  return (
    <Router>
      <Routes>
        {/* Method 3: Using a higher-order component */}
        {/* (The best of both worlds!) */}
        <Route path="/grand-finale" element={<GrandFinaleRoute />} />
      </Routes>
    </Router>
  )
}

The GrandFinaleRoute is concisely coded, resource-efficient, and performant, thus achieving all of our goals.

Routing in React Achieved

Application routing implementations can be coded naively or elegantly, like any other code. We have surveyed the basics of routing as a full exploration of the code for simple and complex React Router-based implementations.

I hope the final routing approach resonates with your desire to bring a beautiful, low-maintenance routing solution to your application. Regardless of the method, you can quickly grade your routing implementation’s effectiveness and security by comparing it to our various examples. Routing in React doesn’t have to be an uphill path.

The Toptal Engineering Blog extends its gratitude to Marco Sanabria for reviewing the repository and code samples presented in this article.

Further Reading on the Toptal Engineering Blog:

Understanding the basics

Developers use the React Router library to manage screen flow within an application.

React Router is a major timesaver. It ensures developers do not have to code their routing logic from scratch.

The React Router library is the most popular routing implementation used by developers. It has the greatest support worldwide.

React Router was originally developed by two key React team members, Michael Jackson and Ryan Florence. Currently, the React Router code base is maintained by Remix Software.

React Router must be installed through a simple “npm install” command, after which routes in React are implemented declaratively with JSX elements.

Comments

Ruben Dario Carvajal Herrera
genial
Patrick Mencias-lewis
Why not create a new Route component which extends the existing Route but adds props which can be used like <code> <Routes> <AuthRoute path="/example-1" element={<CustomComponentPage />} /> <Route path="/example-2" element={<CustomComponentPage />} auth /> <Route path="/example-3" element={<CustomComponentPage />} auth permission={[ 'view.object' ]} /> </Routes> </code>
Nathan Babcock
@patrickmenciaslewis:disqus, great question, in fact this is the exact approach I used to take in React Router v5 and earlier versions. However, in v6 of React Router they added a new error message that prevents this from working: <code>router.ts:5 Uncaught Error: [CustomRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment></code> Unfortunately this locks you into using only the official React Router "Route" component as the foundation for everything else.
Nathan Babcock
@disqus_OfDrbRtJqG:disqus, totally valid point. Wrapping every component inside "withLoggedIn" can become a lot of boilerplate if you have many protected routes in your project. In cases like that, I would make a small modification to make the syntax even more concise. Instead of defining each route before passing it into the Route component, I would do it all inline, inside the JSX. Like this: <code><Route path="/grand-finale" element={withLoggedIn(HigherOrderComponentPage)()} /></code> Now, only a single line of code is needed for each Route. It is also self-documenting in a way; it tells you what page the route corresponds to ("HigherOrderComponentPage"), and also the "withLoggedIn" annotation tells you that authentication is required, so you get all the information you need from a single glance at the code.
Oleg Zaimkin
The final sample, how does it work? GrandFinaleRoute is evaluated in global scope, and it does access useEffect in global scope. How would it evaluate and how is it expected to re-evaluate “condition” after user gets logged in?
comments powered by Disqus