Protect a route with a RouteGuard component
The intent
Restricting access to specific areas of an application is a common requirement in most modern web applications. In this article, we will build a reusable RouteGuard component that acts as a wrapper around the Route component from react-router, allowing us to protect routes from unauthorized access in a clean and declarative way.
The goal is to centralize access-control logic while keeping route definitions simple and readable.
Create your basic routes
First, bootstrap your application using the tool of your choice. For a quick setup, I recommend Vite. Alternatively, you can use CodeSandbox, which provides an excellent online development environment.
Create a components folder in your project root and add a few basic components that will later be mapped to application routes.
Home Component:
// ./components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';
const Home = () => <Link to="/protected">Go to protected area</Link>;
export default Home;
Login component:
// ./components/Login.js
import React from 'react';
const Login = () => <h2>Please login</h2>;
export default Login;
Protected component:
Create another component named Protected. This component represents a section of the application that should only be accessible to authorized users.
// ./components/Protected.js
import React from 'react';
const Protected = () => <h1>Hello, I am the protected component </h1>;
export default Protected;
Now, update your main entry file (src/index.{js,tsx}) to define the initial routes.
import React from 'react';
import { render } from 'react-dom';
import { Route, BrowserRouter as Router, Switch } from 'react-router-dom';
// basic components
import Home from './components/Home';
import Login from './components/Login';
render(
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/user/login">
<Login />
</Route>
</Switch>
</Router>,
document.getElementById('root')
);
At this point, you should be able to navigate to / and /user/login and see the corresponding components rendered correctly.
Create a Mock Access Service
Next, create a simple service to simulate user access control.
Inside your project root, create a services folder and add a UserService. This service will mock authentication logic by returning a random boolean value. This approach is not mandatory and is used only to keep the example focused and self-contained.
class UserService {
// passing a force param that will be returned after the timeout
checkAccess = async (force) => {
// check / revalidate / refresh a token, execute a request... everything you want
return new Promise((resolve) => {
// create a random boolean value to simulate the result of your logic
const rand = !Math.round(Math.random());
setTimeout(() => {
resolve(force ? force : rand);
}, 1500);
});
};
}
export default new UserService();
Create the RouteGuard Component
The RouteGuard component wraps Route and determines whether the target component should be rendered or the user should be redirected.
import React, { useEffect, useState, useCallback } from 'react';
import { Navigate, Outlet, useLocation } from 'react-router-dom';
type AuthorizeFn = () => boolean | Promise<boolean>;
type RedirectTo = string | ((isAuthorized: boolean) => void);
interface RouteGuardProps {
/**
* Function responsible for determining authorization.
* Can be sync or async.
*/
authorize: AuthorizeFn;
/**
* Redirect path or callback when unauthorized.
*/
redirectTo: RedirectTo;
/**
* Optional loader toggle.
*/
showLoader?: boolean;
/**
* Optional query string appended to redirect.
*/
query?: string;
/**
* Optional test id wrapper.
*/
testId?: string;
/**
* Optional inline element.
* If omitted, <Outlet /> is rendered.
*/
element?: React.ReactElement;
}
const RouteGuard: React.FC<RouteGuardProps> = ({
authorize,
redirectTo,
showLoader = true,
query = '',
testId,
element,
}) => {
const location = useLocation();
const [isAuthorized, setIsAuthorized] = useState(false);
const [validationCompleted, setValidationCompleted] = useState(false);
useEffect(() => {
let isMounted = true;
const runAuthorization = async (): Promise<void> => {
try {
const result = await authorize();
if (isMounted) {
setIsAuthorized(Boolean(result));
setValidationCompleted(true);
}
} catch {
if (isMounted) {
setIsAuthorized(false);
setValidationCompleted(true);
}
}
};
runAuthorization();
return () => {
isMounted = false;
};
}, [authorize]);
const handleRedirect = useCallback((): JSX.Element | null => {
if (typeof redirectTo === 'function') {
redirectTo(isAuthorized);
return null;
}
return (
<Navigate
replace
to={{
pathname: redirectTo,
search: query,
}}
state={{ from: location }}
/>
);
}, [redirectTo, isAuthorized, query, location]);
if (!validationCompleted) {
return showLoader ? <span>Loading...</span> : null;
}
if (!isAuthorized) {
return handleRedirect();
}
const content = element ?? <Outlet />;
return testId ? <div data-testid={testId}>{content}</div> : content;
};
export default RouteGuard;
Update your routes
Now, integrate the RouteGuard component into your routing configuration.
When a user navigates to the protected route, the access check is performed before rendering the component.
import { BrowserRouter as Router, Routes, Route } from 'react-router';
import RouteGuard from './RouteGuard';
import UserService from './services/UserService';
import Home from './Home';
import Login from './Login';
import Protected from './Protected';
export default function AppRouter() {
return (
<Router>
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/user/login" element={<Login />} />
{/* Protected routes */}
<Route
element={
<RouteGuard
authorize={() => UserService.checkAccess()}
redirectTo="/user/login"
testId="protected-route"
query="?param=1¶m=2"
// callback alternative
// redirectTo={(authorized) =>
// console.log(authorized ? 'ok nice' : 'Sorry, who are you?')
// }
/>
}
>
{/* render in the <Outlet /> */}
<Route path="/protected" element={<Protected />} />
</Route>
{/* Inline Guard (single route) */}
{/*
<Route
path="/protected"
element={
<RouteGuard
authorize={() => UserService.checkAccess()}
redirectTo="/user/login"
testId="protected-route"
element={<Protected />}
/>
}
/>
*/}
</Routes>
</Router>
);
}