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 replacesreact-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:
- 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.
- Other
- 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.
- 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.