Editor’s note: This article was updated on 25 January 2024 by Elijah Asaolu to ensure all information is in line with the most recent versions of React and React Router, demonstrate how to implement two-factor authentication, troubleshoot some common issues while authentication with React Router, and more.
React Router v6 is a popular and powerful routing library for React applications. It provides a declarative, component-based approach to routing and handles the common tasks of dealing with URL params, redirects, and loading data.
React Router provides one of the most intuitive APIs available and enables lazy loading and SEO-friendly server-side rendering. This latest version of React Router introduced many new concepts, like <Outlet />
and layout routes, but the documentation is still sparse.
This tutorial will demonstrate how to create protected routes and add authentication with React Router v6.
So, fire up your favorite text editor, and let’s dive in!
Open up the terminal and create a new React project by running the following command:
npm create vite@latest ReactRouterAuthDemo -- --template react cd ReactRouterAuthDemo
Next, install React Router as a dependency in the React app:
npm install react-router-dom
Once the React Router dependency is installed, we’ll need to edit the src/main.js
file.
Import BrowserRouter
from react-router-dom
and then wrap the <App />
component with <BrowserRouter />
, like so:
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; import { BrowserRouter } from "react-router-dom"; ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> );
Now, we’re all set up to use React Router components and Hooks from anywhere in our app.
Let’s replace the boilerplate code from the App.js
file with some routes.
React Router provides the <Routes />
and <Route />
components that enable us to render components based on their current location:
// src/App.jsx import { Routes, Route } from "react-router-dom"; import { LoginPage } from "./pages/Login"; import { HomePage } from "./pages/Home"; import "./App.css"; function App() { return ( <Routes> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> </Routes> ); } export default App;
<Route >
<Route />
provides the mapping between paths on the app and different React components. For example, to render the LoginPage
component when someone navigates to /login
, we just need to provide the <Route />
, like so:
<Route path="/login" element={<LoginPage />} />
The <Route />
component can be thought of like an “if” statement. It will act upon a URL location with its element only if it matches the specified path.
<Routes />
The <Routes />
component is an alternative to the <Switch />
component from React Router v5.
To use <Routes />
, we’ll first create Login.jsx
and Home.jsx
files inside the pages directory with the following content:
// Login.jsx export const LoginPage = () => ( <div> <h1>This is the Login Page</h1> </div> ); // Home.jsx export const HomePage = () => ( <div> <h1>This is the Home Page</h1> </div> );
Next, we’ll run this command to start the app:
npm run dev
On the browser, we see the HomePage
component by default. If we go the /login
route we’ll see the LoginPage
component render on the screen.
Alternatively, we can use a plain JavaScript object to represent the routes in our app using the useRoutes
Hook. This is a functional approach for defining routes and works in the same manner as the combination of <Routes />
and <Route />
components:
// src/App.jsx import { useRoutes } from "react-router-dom"; // ... export default function App() { const routes = useRoutes([ { path: "/", element: <HomePage /> }, { path: "/login", element: <LoginPage /> } ]); return routes; }
Now that the basic setup is completed, let’s look at how we can create protected routes so that unauthenticated users cannot access certain content in our application.
Protected routes, often known as private routes, are a fundamental concept in web development used to restrict access to certain pages or resources to authenticated users only.
To implement a protected route in our project, let’s start by creating a custom useAuth
Hook to manage the authenticated user’s state using React’s Context API and useContext
Hook:
// src/hooks/useAuth.jsx import { createContext, useContext, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useLocalStorage } from "./useLocalStorage"; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useLocalStorage("user", null); const navigate = useNavigate(); // call this function when you want to authenticate the user const login = async (data) => { setUser(data); navigate("/profile"); }; // call this function to sign out logged in user const logout = () => { setUser(null); navigate("/", { replace: true }); }; const value = useMemo( () => ({ user, login, logout, }), [user] ); return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }; export const useAuth = () => { return useContext(AuthContext); };
This useAuth
Hook exposes the user’s state and methods for user login and logout. When users successfully log in, the login()
method modifies their state to reflect their authentication status. In addition, when users log out, we redirect them to the homepage using React Router’s useNavigate
Hook.
To maintain the user’s state even after a page refresh, let’s create the useLocalStorage
Hook, which synchronizes the state value with the browser’s local storage:
// src/hooks/useLocalStorage.jsx import { useState } from "react"; export const useLocalStorage = (keyName, defaultValue) => { const [storedValue, setStoredValue] = useState(() => { try { const value = window.localStorage.getItem(keyName); if (value) { return JSON.parse(value); } else { window.localStorage.setItem(keyName, JSON.stringify(defaultValue)); return defaultValue; } } catch (err) { return defaultValue; } }); const setValue = (newValue) => { try { window.localStorage.setItem(keyName, JSON.stringify(newValue)); } catch (err) { console.log(err); } setStoredValue(newValue); }; return [storedValue, setValue]; };
Next, let’s create the ProtectedRoute
component, which checks the current user’s state from the useAuth
Hook and redirects them to the homescreen if they are not authenticated:
import { Navigate } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; export const ProtectedRoute = ({ children }) => { const { user } = useAuth(); if (!user) { // user is not authenticated return <Navigate to="/login" />; } return children; };
In the code above, we use React Router’s <Navigate />
component to redirect unauthenticated users to the /login
route.
With the base structure in place, the next step is to add a LoginPage
route for user authentication and a Secret
route that is only visible to logged-in users.
Create a file named Login.jsx
within your pages directory and paste the following code into it:
import { useState } from "react"; import { useAuth } from "../hooks/useAuth"; export const LoginPage = () => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const { login } = useAuth(); const handleLogin = async (e) => { e.preventDefault(); // Here you would usually send a request to your backend to authenticate the user // For the sake of this example, we're using a mock authentication if (username === "user" && password === "password") { // Replace with actual authentication logic await login({ username }); } else { alert("Invalid username or password"); } }; return ( <div> <form onSubmit={handleLogin}> <div> <label htmlFor="username">Username:</label> <input id="username" type="text" value={username} onChange={(e) => setUsername(e.target.value)} /> </div> <div> <label htmlFor="password">Password:</label> <input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> <button type="submit">Login</button> </form> </div> ); };
This component serves as the user’s login interface. It uses the useAuth
Hook to handle user authentication. When users enter their credentials and submit the form, the login()
function from useAuth
is called to authenticate and log them in.
Similarly, create a Secret.jsx
file under the pages directory to represent a secured page that displays content exclusively to authenticated users:
import { useAuth } from "../hooks/useAuth"; export const Secret = () => { const { logout } = useAuth(); const handleLogout = () => { logout(); }; return ( <div> <h1>This is a Secret page</h1> <button onClick={handleLogout}>Logout</button> </div> ); };
We also added a logout button on our Secret
page above, allowing users to sign out when necessary. This logout action will be handled by the logout()
method from the useAuth
Hook.
Finally, in your App.jsx
file, encapsulate all routes within the AuthProvider
from our previously created useAuth
Hook to provide a consistent authentication context across your app. Set up your routes as usual, and for routes that require authentication, use the <ProtectedRoute />
component to restrict access to only authenticated users:
// src/App.jsx import { Routes, Route } from "react-router-dom"; import { LoginPage } from "./pages/Login"; import { HomePage } from "./pages/Home"; import { Secret } from "./pages/Secret"; import "./App.css"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { AuthProvider } from "./hooks/useAuth"; function App() { return ( <AuthProvider> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> <Route path="/secret" element={ <ProtectedRoute> <Secret /> </ProtectedRoute> } /> </Routes> </AuthProvider> ); } export default App;
With these steps, we’ve created a basic authentication flow that only allows authenticated users to access protected routes. If you try to access /secret
without logging in, you will be instantly redirected to the login page. However, you can access the secret page once you enter the default username and password into the login form.
The above approach works fine if there are a limited number of protected routes. However, if there were multiples of such routes, we would have to wrap each one, which is tedious. To fix this, we can use the React Router v6 nested route feature to wrap all the protected routes in a single layout.
Let’s enhance our application by integrating two-factor authentication (2FA) with React Router. 2FA adds an extra layer of security by requiring users to provide two distinct forms of identification before accessing sensitive features.
To proceed, let’s modify the existing authentication setup to include 2FA. Update your useAuth.jsx
file with the following code:
// src/hooks/useAuth.jsx import { createContext, useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useLocalStorage } from "./useLocalStorage"; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useLocalStorage("user", null); const [is2FAVerified, setIs2FAVerified] = useState(false); const navigate = useNavigate(); const login = async (data) => { setUser(data); // Navigate to 2FA verification page navigate("/verify-2fa"); }; const logout = () => { setUser(null); setIs2FAVerified(false); navigate("/", { replace: true }); }; const verify2FACode = async (code) => { // Mock verification logic if (code === "0000") { setIs2FAVerified(true); navigate("/secret"); // Navigate to a protected route after successful 2FA return true; } return false; }; const value = { user, is2FAVerified, login, logout, verify2FACode, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }; export const useAuth = () => { return useContext(AuthContext); };
In this updated file, we’ve modified the login method for 2FA and added a mock verify2FACode()
function, which approves the code 0000
for simplicity. In a real scenario, this is where you’d implement actual 2FA verification, like sending a code via SMS or email.
Next, let’s add a new page component that allows users to enter the 2FA code sent to them:
import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; export const Verify2FA = () => { const navigate = useNavigate(); const { verify2FACode } = useAuth(); const [code, setCode] = useState(""); const handleSubmit = async (e) => { e.preventDefault(); const isValid = await verify2FACode(code); if (isValid) { navigate("/secret"); } else { alert("Invalid code. Please try again."); } }; return ( <form onSubmit={handleSubmit}> <input type= "text" value={code} onChange={(e) => setCode(e.target.value)} placeholder= "Enter verification code" /> <button type="submit">Verify</button> </form> ); };
We also need to update our ProtectedRoute
component to integrate the 2FA verification logic:
import { Navigate } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; export const ProtectedRoute = ({ children }) => { const { user, is2FAVerified } = useAuth(); if (!user) { return <Navigate to="/login" />; } if (!is2FAVerified) { return <Navigate to="/verify-2fa" />; } return children; }; export default ProtectedRoute;
With the new Verify2FA
component and our ProtectedRoute
component set up, modify your App.jsx
route settings to include the verify-2fa
route definition, like below:
import { Routes, Route } from "react-router-dom"; import { LoginPage } from "./pages/Login"; import { HomePage } from "./pages/Home"; import { Secret } from "./pages/Secret"; import { Verify2FA } from "./pages/Verify2FA"; import "./App.css"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { AuthProvider } from "./hooks/useAuth"; function App() { return ( <AuthProvider> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/verify-2fa" element={<Verify2FA />} /> <Route path="/login" element={<LoginPage />} /> <Route path="/secret" element={ <ProtectedRoute> <Secret /> </ProtectedRoute> } /> </Routes> </AuthProvider> ); } export default App;
And that’s it! With the updated setup, unauthenticated users are redirected to the login page when they try to access the protected /secret
route. If they’re logged in but have yet to pass 2FA, they’re redirected to the 2FA verification page. They can only access protected routes once both authentication steps are completed.
Another common authentication pattern is integrating React Router with third-party authentication libraries like Auth0. The process involves creating an Auth0 account, retrieving your credentials, and utilizing libraries such as auth0-react
to implement the authentication process seamlessly.
While integrating Auth0 with React Router is outside the scope of this article, learning to do so could be a useful next step. For a comprehensive guide and to begin your setup, refer to the official Auth0 documentation.
<Outlet />
One of the most powerful features in React Router v6 is nested routes. This feature allows us to have a route that contains other child routes. The majority of our layouts are coupled to segments on the URL, and React Router supports this fully.
For example, we can add a parent <Route />
component to the <HomePage />
and <LoginPage />
routes, like so:
import { ProtectedLayout } from "./components/ProtectedLayout"; import { HomeLayout } from "./components/HomeLayout"; // ... export default function App() { return ( <Routes> <Route element={<HomeLayout />}> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> </Route> <Route path="/dashboard" element={<ProtectedLayout />}> <Route path="profile" element={<ProfilePage />} /> <Route path="settings" element={<SettingsPage />} /> </Route> </Routes> ); }
The parent <Route />
component can also have a path and is responsible for rendering the child <Route />
component on the screen.
When the user navigates to /dashboard/profile
, the router will render the <ProfilePage />
. For this to occur, the parent route element must have an <Outlet />
component to render the child elements. The Outlet
component enables nested UI elements to be visible when child routes are rendered.
The parent route element can also have additional common business logic and user interface. For example, in the <ProtectedLayout />
component, we have included the private route logic along with a common navigation bar that will be visible when the child routes are rendered:
import { Navigate, Outlet } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; export const ProtectedLayout = () => { const { user } = useAuth(); if (!user) { return <Navigate to="/" />; } return ( <div> <nav> <Link to="/settings">Settings</Link> <Link to="/profile">Profile</Link> </nav> <Outlet /> </div> ) };
Instead of the <Outlet />
component, we can also opt to use the useOutlet
Hook, which serves the same purpose:
import { Link, Navigate, useOutlet } from "react-router-dom"; // ... export const ProtectedLayout = () => { const { user } = useAuth(); const outlet = useOutlet(); if (!user) { return <Navigate to="/" />; } return ( <div> <nav> <Link to="/settings">Settings</Link> <Link to="/profile">Profile</Link> </nav> {outlet} </div> ); };
Similar to protected routes, we don’t want authenticated users to access the /login
path. Let’s handle that in the <HomeLayout />
component:
import { Navigate, Outlet } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; export const HomeLayout = () => { const { user } = useAuth(); if (user) { return <Navigate to="/dashboard/profile" />; } return ( <div> <nav> <Link to="/">Home</Link> <Link to="/login">Login</Link> </nav> <Outlet /> </div> ) };
You can check out the complete code and demo via CodeSandbox.
In v6.4, the React Router package introduced new routers and data APIs. Going forward, all web apps should use the createBrowserRouter()
function to enable data API access.
The fastest way to update an existing app to the new React Router API is by wrapping the Route
components with the createRoutesFromElements()
function:
export const router = createBrowserRouter( createRoutesFromElements( <> <Route element={<HomeLayout />}> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> </Route> <Route path="/dashboard" element={<ProtectedLayout />}> <Route path="profile" element={<ProfilePage />} /> <Route path="settings" element={<SettingsPage />} /> </Route> </> ) );
In the index.js
file, instead of the <BrowserRouter />
component, use the <RouterProvider />
component and pass the exported router
object from the App.js
file. Also, note that the AuthProvider
will not work without BrowserRouter
since it uses the useNavigate()
function:
import { router } from "./App"; ... root.render( <StrictMode> <ThemeProvider theme={theme}> <RouterProvider router={router} /> </ThemeProvider> </StrictMode> );
To use the AuthProvider
within the router context, we’ll need to create an <AuthLayout />
component that will wrap the outlet
element with AuthProvider
. This will enable all the child routes to have access to the AuthContext
:
import { useLoaderData, useOutlet } from "react-router-dom"; import { AuthProvider } from "../hooks/useAuth"; export const AuthLayout = () => { const outlet = useOutlet(); return ( <AuthProvider>{outlet}</AuthProvider> ); };
Now, we can use the AuthLayout
component as a root-level route, like so:
export const router = createBrowserRouter( createRoutesFromElements( <Route element={<AuthLayout />} > <Route element={<HomeLayout />}> ... </Route> <Route path="/dashboard" element={<ProtectedLayout />}> ... </Route> </Route> ) );
At this point, the app is ready to access the data APIs.
With React Router’s data API, we can abstract how the data is fetched. Usually, we’d load the data inside our component using the useEffect
Hook. Instead, we can use the Router’s loader()
function to fetch the data before rendering the route element.
Consider a use case in which we need to get the logged-in user’s data when the application loads. Depending on whether the user is authenticated, we can redirect them to either the homepage or the dashboard.
To simulate data fetching, we can use Promise
with the setTimeout()
method and get the user
from localStorage
:
const getUserData = () => new Promise((resolve) => setTimeout(() => { const user = window.localStorage.getItem("user"); resolve(user); }, 3000) );
Using the loader
prop on the Route
component, we can pass the Promise
— getUserData()
— to the AuthLayout
component with the help of the defer()
utility function. The defer()
function allows us to pass promises instead of resolved values before the Route
component is rendered:
import { Route, createBrowserRouter, createRoutesFromElements, defer } from "react-router-dom"; import { AuthLayout } from "./components/AuthLayout"; ... // ideally this would be an API call to server to get logged in user data const getUserData = () => new Promise((resolve) => setTimeout(() => { const user = window.localStorage.getItem("user"); resolve(user); }, 3000) ); export const router = createBrowserRouter( createRoutesFromElements( <Route element={<AuthLayout />} loader={() => defer({ userPromise: getUserData() })} > <Route element={<HomeLayout />}> ... </Route> <Route path="/dashboard" element={<ProtectedLayout />}> ... </Route> </Route> ) );
In the AuthLayout
component, you can access the userPromise
using the useLoaderData
Hook.
The Await
component can render deferred values with an inbuilt error handling mechanism. The Await
component should be wrapped in React Suspense to enable a fallback UI. In this case, we’re rendering a linear progress bar until the userPromise
is resolved.
We can pass a component to the errorElement
prop to render an error UI state if the Promise
gets rejected.
Finally, we can pass the user data as an initial value to the AuthProvider
:
import { Suspense } from "react"; import { useLoaderData, useOutlet, Await } from "react-router-dom"; import LinearProgress from "@mui/material/LinearProgress"; import Alert from "@mui/material/Alert"; import { AuthProvider } from "../hooks/useAuth"; export const AuthLayout = () => { const outlet = useOutlet(); const { userPromise } = useLoaderData(); return ( <Suspense fallback={<LinearProgress />}> <Await resolve={userPromise} errorElement={<Alert severity="error">Something went wrong!</Alert>} children={(user) => ( <AuthProvider userData={user}>{outlet}</AuthProvider> )} /> </Suspense> ); };
To verify the error condition, you can reject the Promise
as shown below:
// for error const getUserData = () => new Promise((resolve, reject) => setTimeout(() => { reject("Error"); }, 3000) );
You can also check out the complete code for the 2FA authentication example in this GitHub repo and the data library integration example in this CodeSandbox.
It’s worth investing some time to better understand how React Router v6 works, particularly for the common use case of user authentication.
React Router v6 is a huge improvement over previous versions. It’s fast, stable, and reliable. In addition to being easier to work with, it has many new features, like <Outlet />
and an improved <Route />
component, that have greatly simplified routing in React apps.
With new routers and data APIs available in v6.4, you can easily handle optimistic UI, pending, and error states. You can abstract and load data outside the component while displaying a fallback UI until the data is ready.
I hope you found this guide helpful and now have a better understanding of how to handle user authentication with React Router v6.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
Build a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend.
console.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
15 Replies to "Authentication with React Router v6: A complete guide"
In the example where login path is removed after login, the home page is no longer accessible. To reproduce, after login, try to go to home page. It will always redirect to /dashboard/profile. I think this shouldnt be the ideal scenario.
Thanks for this – an alternative to making an AuthLayout file etc for RRD > v. 6.4 (RouterProvider) consider this instead:
…. <Route
path="/"
element={
}
>
Wrapping AuthProvider around the ProtectedLayout
i cant use this scenario because im using a theme that used react router dom v6 and useRoutes i dont have routes and route jsx and i cant replace them
I’m getting the following error: React Hook React.useMemo has missing dependencies: ‘login’ and ‘logout’ Either include them or remove them from the dependency array. Do you have a solution for this error?
I think you need to wrap with , as shown in your code sandbox: https://codesandbox.io/s/react-router-v6-auth-demo-4jzltb?file=/src/index.js:498-519
Don’t Try this tutorial, it is not authenticating Token, Only it generate a token and store it in local storage.. this is not the technically correct
Hey Sooraj, the actual authentication of the user is not in scope of the article. This article only demonstrates how you’d handle authentication and private routes on the frontend using React Router.
I did the tutorial up to:
‘Well, the above approach works fine if there are a limited number of protected routes, but if there are multiple such routes we would have to wrap each one, which is tedious.’
The code prior to that doesn’t work as in component I get the error:
`uncaught TypeError: Cannot destructure property ‘user’ of ‘(0 , _hooks_useAuth__WEBPACK_IMPORTED_MODULE_0__.useAuth)(…)’ as it is undefined.`
I had same issue, I solve it by add AuthProvider in your index.js.
what if i am using the createBrowserRouter() function?
example:
const router = createBrowserRouter([
{
path: “/”,
element: ,
errorElement: ,
},
{
path: “dashboard”,
element: ,
children: [
{ path: “myresources”, element: },
{ path: “users”, element: , loader: usersLoader },
{ path: “resources”, element: },
{
path: “users/:userId”,
element: ,
loader: userLoader,
id: “userId”,
children: [
{
path: “edit”,
element: ,
action: editAction,
},
],
},
{ path: “users/add”, element: , action: addAction },
],
},
]);
const root = ReactDOM.createRoot(
document.getElementById(“root”) as HTMLElement
);
root.render(
);
I have the same question, how to do auth0 with react-router 6.4+ (RouterProvide features?)
Why are you using `children` and not React Router’s “ in the AuthProvider?
I am having an issue after following this and using react-query to do the back end call for authentication.
On logout, setData(null) isn’t working. The users data is still here and then we get redirected BACK to the protected page. no way to actually log out.
Hi Dan. I’m not sure about your integration with react-query and would need additional details to assist you there. However, you’ll need to modify the logout() function to include your own custom logout logic. For example, if you’re saving user data in a session or cookie, you must clear it using the appropriate method.
If anyone trying 2FA, ProtectedRoute, use verify2FACode instead of IsVerify2FACode so you can filter if statement for both login password and 2FA.