Master the art of redirecting with React Router Tutorial

React Router is the leading solution for managing page transitions and routing within React applications. Since its inception in 2014, React Router has evolved alongside React, becoming a cornerstone of the React ecosystem.

This tutorial commences by elucidating a fundamental routing concept and justifying the selection of React Router. Subsequently, it provides a step-by-step guide on constructing a rudimentary application that effectively demonstrates various routing functionalities. Lastly, it delves into the implementation of an elegant, secure, and reusable component designed to achieve a minimally intrusive and easily maintainable routing solution. The resulting routing code adheres to React’s coding principles and style, seamlessly integrating into any modern React application.

Getting Started: Declarative Routing Basics

Declarative routing is the coding style employed within React and React Router. In React, declarative routes are components, leveraging the same mechanisms available in any React application. This consistency simplifies their usage.

These routes establish associations between web addresses and specific pages or components. They harness React’s powerful rendering capabilities and conditional logic to dynamically activate or deactivate routes as needed. This conditional routing in React empowers developers to implement application logic that ensures route accuracy and security.

Naturally, the effectiveness of any router hinges on its underlying library. While many developers overlook quality of life considerations when selecting a library, React Router v6 distinguishes itself by offering a wealth of powerful features that streamline routing tasks, making it the optimal choice for React routing.

What sets React Router apart from other routing libraries?

  • It embraces declarative route definitions using JSX within React components.
  • It holds the status of the industry standard.
  • It boasts an abundance of code examples and online tutorials.
  • It adheres to modern React code conventions, employing hooks and functional components.

For developers transitioning from React Router v5, it’s important to note three key changes in v6:

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

The subsequent sections of this article will explore various v6-compatible patterns, culminating in the presentation of our ultimate and most elegant route composition. For a comprehensive guide on upgrading from v5 to v6, refer to the official migration guide.

Time to Set Up a Basic React Application

Every React routing tutorial necessitates a basic foundation to showcase its intended features. Assuming your development environment has npm installed, let’s proceed by creating a simple React project with Vite—no need for a separate Vite installation—to provide us with the fundamental React app structure, a standalone web server, and all the necessary dependencies:

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

This command generates our base app using TypeScript.

React Routes Basics

React Router’s primary function is to redirect users to different pages within a client-side application based on associated web addresses. The routing logic of an application encompasses both general program logic and the handling of requests for non-existent pages (i.e., redirecting to a 404 page).

Despite React’s single-page application (SPA) nature, these routes effectively simulate the behavior of traditional web applications with separate physical or file-based routing. This approach ensures that end users perceive the application as a website with distinct pages while preserving the advantages of SPAs, such as seamless page transitions. The React Router library further guarantees that browser history remains accessible and the back button functions as expected.

Protect Your React Route

Within a single-page application, React Routes control access to specific components, effectively governing the information and functionality accessible to end users. It is crucial to ensure that users can only access features authorized by the system’s security requirements.

While client-side security is paramount in our React client, a robust security implementation should always include additional (and arguably more critical) server-side security measures to guard against unauthorized client-side manipulation. Unforeseen circumstances can arise, and resourceful users might attempt to probe the application’s vulnerabilities using browser development tools. Prioritizing security is essential.

Client-side administrative functions serve as a prime example. Access to these functions should be strictly regulated through system authentication and authorization mechanisms. Only authorized system administrators should have access to potentially sensitive or destructive system operations.

The Easy Solution You Shouldn’t Choose

The React developer community encompasses a wide spectrum of expertise. Novice React developers, in particular, often gravitate towards less elegant coding styles, especially when it comes to routes and their associated secure access logic.

Common characteristics of such naive implementations include:

  • Defining route protection logic on every individual page.
  • Relying on useEffect React hooks to handle page redirection when unauthorized access is detected.
  • Requiring the entire page to load before redirect and route protection logic is executed.

A naive routing component implementation might resemble the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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>
  )
}

Within an application, this routing component would be utilized as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export default function App() {
  return (
    <Router>
      <Routes>
        {/* Method 1: Using `useEffect()` as a redirect */}
        <Route path="/naive-approach" element={<NaiveApproach />} />
      </Routes>
    </Router>
  )
}

Despite its prevalence, this approach should be avoided due to its adverse effects on system performance and user experience. Naive routing introduces three primary drawbacks:

  1. Performance Degradation:
    • Other useEffect hooks might execute before the React redirect occurs, potentially delaying the redirection process.
    • Unnecessary server-side requests can lead to system slowdowns. Depending on the complexity of the logic executed before security checks, performance degradation of 75% or more is not uncommon.
  2. Flickering Issues:
    • As the protected page loads initially, a brief navigation to the requested address might occur. Depending on the page’s security logic, a subsequent redirect might be triggered, leading to a noticeable flicker.
  3. Code Duplication:
    • Implementing this routing logic on every protected page throughout the application results in code redundancy, creating a maintenance nightmare.

Better React Routing With a Custom Component

To enhance the elegance of our secure routing, we should focus on three key aspects: minimizing code maintenance, centralizing secure routing logic for reduced code footprint, and improving overall application performance. Introducing a custom ProtectedRoute component allows us to address these objectives effectively:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 modification to leverage this new ConditionalRoute component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 offers substantial improvements over the previous naive approach due to the following reasons:

  • Centralized Security Logic: Secure routing implementation is consolidated within a single component, greatly simplifying code maintenance.
  • Prevention of Unauthorized Routes: This focused page routing logic potentially eliminates unnecessary server calls and page rendering operations by intercepting unauthorized requests.

While superior to naive solutions, this implementation is not yet ideal. The usage pattern demonstrated in our application code sample introduces more code bloat than desired, motivating us to explore an even more elegant solution.

The Best React Router Solution

Our ultimate goal is to achieve an exceptional, top-tier implementation that embodies the principles of highly componentized route security, flexible parameter handling, and minimal intrusion within pages requiring routing. To this end, we introduce the meticulously crafted and minimally invasive GrandFinaleRoute component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** 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 fulfills all of our requirements but also offers a remarkably elegant and concise usage pattern within our page components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 excels in its conciseness, resource efficiency, and performance, effectively achieving all of our objectives.

Routing in React Achieved

As with any aspect of software development, application routing implementations can range from rudimentary to sophisticated. We have explored the fundamentals of routing, providing a comprehensive overview of the code for both simple and complex React Router-based implementations.

It is our hope that this final routing approach resonates with your desire to implement a robust, maintainable, and user-friendly routing solution within your application. Regardless of the chosen method, you can readily assess the effectiveness and security of your routing implementation by comparing it against the various examples provided. Routing in React need not be an arduous endeavor. We encourage you to share your insights, experiences, and React Router best practices in the comments section below.

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

Licensed under CC BY-NC-SA 4.0