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.
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.
Nathan is a front-end and full-stack developer, and an expert in streamlining UI/UX with React. As the lead design engineer at Motorola Solutions, he marshaled an internal product that included React, Angular, Svelte, and native web components from prototype to corporatewide deployment—garnering more than 100,000 downloads.
Expertise
Previously At
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. Conditional routing in React 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 v7, the current stable release, 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, functional components, and a unified
react-routerpackage that replaces the separatereact-router-dominstall).
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-configfor 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.
React Router v7, released in November 2024, builds on v6’s Data Router foundation and is the version used in the Grand Finale below. If you’re on v6.4 or later, the migration is minimal. See the official v7 upgrade guide.
Time to Set Up a Basic React Application
Every React routing 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
useEffectReact 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
useEffecthooks could potentially run before the redirect in React 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
The higher-order component pattern above is a significant improvement, but it still has one blind spot: auth runs inside the React render cycle. That means the component tree starts mounting before the redirect happens, creating a brief flash of unauthorized content. React Router v7 solves this at the data layer with createBrowserRouter and loader functions. Loaders run before the component renders, so unauthorized users are redirected before a single pixel of protected content is painted.
First, add a small bridge to UserContext so loaders, which run outside the React tree, can read auth state. The useContext hook cannot be called outside a component, so we expose a module-level store that UserContext keeps in sync:
// UserContext.tsx (updated)
import { createContext, useState } from 'react'
// Module-level store readable by route loaders
export const authStore = { loggedIn: false }
export const UserContext = createContext({
loggedIn: false,
setLoggedIn: (_: boolean) => {}
})
export function UserProvider({ children }: { children: React.ReactNode }) {
const [loggedIn, setLoggedIn] = useState(false)
authStore.loggedIn = loggedIn // keep store in sync
return (
<UserContext.Provider value={{ loggedIn, setLoggedIn }}>
{children}
</UserContext.Provider>
)
}
Now define an auth loader. throw redirect() exits immediately and the component never loads:
// authLoader.ts
import { redirect } from 'react-router-dom'
import { authStore } from './UserContext'
export async function authLoader() {
if (!authStore.loggedIn) {
throw redirect('/home')
}
return null
}
Build the router with createBrowserRouter. Protected routes share the auth loader through a parent route, so no per-page logic is required:
// router.tsx
import { createBrowserRouter } from 'react-router-dom'
import { authLoader } from './authLoader'
import { GrandFinalePage } from './pages/GrandFinalePage'
import { HomePage } from './pages/HomePage'
export const router = createBrowserRouter([
{
path: '/',
children: [
{ path: 'home', element: <HomePage /> },
{
// Method 3: Data Router — auth runs before any child renders
loader: authLoader,
children: [
{
path: 'react/react-router-tutorial',
element: <GrandFinalePage />
},
],
},
],
},
])
Finally, replace <BrowserRouter> in your entry point with <RouterProvider>:
// main.tsx
import { RouterProvider } from 'react-router-dom'
import { UserProvider } from './UserContext'
import { router } from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<UserProvider>
<RouterProvider router={router} />
</UserProvider>
)
This Data Router setup is concisely coded, eliminates flash of unauthorized content, and scales cleanly: adding a new protected route means appending one object to the children array with no HOC wrapping and no per-component auth logic.
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. Feel free to share your tips, experiences, and React Router best practices in the comments section.
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 Blog:
- Efficient React Components: A Guide to Optimizing React Performance
- Building Simple and Efficient Components With React-Bootstrap
- Demystifying Debugging With React Developer Tools
- Full-stack NLP With React: Ionic vs. Cordova vs. React Native
- React Tutorial: How It Compares to Angular and Vue
- React Tutorial: Components, Hooks, and Performance
- Heavy Computation Made Lighter: React Memoization
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.
Nathan Babcock
Chicago, IL, United States
Member since December 29, 2021
About the author
Nathan is a front-end and full-stack developer, and an expert in streamlining UI/UX with React. As the lead design engineer at Motorola Solutions, he marshaled an internal product that included React, Angular, Svelte, and native web components from prototype to corporatewide deployment—garnering more than 100,000 downloads.

