Check your version
This post assumes you're using React Router v6. If not, find your version below.

Often times when building a web app, you’ll need to protect certain routes in your application from users who don’t have the proper authentication. Protected routes let us choose which routes users can visit based on whether they are logged in. For example, you might have public routes that you want anyone accessing, like a landing page, a pricing page, and the login page. Protected routes should only be available to users that are logged in, like a dashboard or settings page.

Though React Router doesn’t provide any functionality for this out of the box, because it was built with composability in mind, adding it is fairly straight forward.

Warning

Note that this, or any other solution you write on the front-end, is going to be for UX purposes only. You should have proper checks in place on the server side to make sure users aren’t getting access to data they shouldn’t be.

More info.

Remember, any JavaScript that is in your client (front-end) code is not only accessible, but anyone can update it via the console. This is why it’s not good enough to only check a user’s authentication status using client-side JavaScript, because any developer could open up the console and update it.

This is why it’s important to also have server-side checks in place before you send any data down to your client. No user should get access to privte data unless they have the proper permissions, and by checking on the server, you ensure that’s the case.


Before we go about creating our protected routes, we’ll need a way to figure out if the user is authenticated. Because this is a tutorial about React Router protected routes and not about authentication, we’ll use a fake useAuth Hook to determine our user’s auth “status”.

Though it’s fake, it follows a good pattern of how you might want to implement a useAuth Hook for yourself.

import * as React from "react";
const authContext = React.createContext();
function useAuth() {
const [authed, setAuthed] = React.useState(false);
return {
authed,
login() {
return new Promise((res) => {
setAuthed(true);
res();
});
},
logout() {
return new Promise((res) => {
setAuthed(false);
res();
});
}
};
}
export function AuthProvider({ children }) {
const auth = useAuth();
return (
<authContext.Provider value={auth}>
{children}
</authContext.Provider>;
)
}
export default function AuthConsumer() {
return React.useContext(authContext);
}

Now whenever we want to know if the user is authed, login, or logout, we can use the useAuth Hook.

More on useAuth

There are lots of different ways the useAuth Hook could work.

Perhaps it makes an HTTP Fetch request to an API endpoint to validate a cookie. Or maybe it decodes a JWT token stored in the browser’s localstorage. Or you could be using a third-party auth solution, like Firebase, and the useAuth Hook just exposes values from that library.

In any case, the goal is the same: find out if the user is currently authenticated.


Now that that’s out of the way, let’s start building out the rest of our app. We’ll have 5 components, Home, Pricing, Dashboard, Settings, and Login, which will map nicely to our 5 routes,/, /pricing, /dashboard, /settings, and /login.

The /, /pricing, and /login routes will be publicly accessible while our /dashboard and /settings route will be private. For now, we’ll just render them like normal Routes though.

import * as React from "react";
import { Link, Routes, Route } from "react-router-dom";
const Home = () => <h1>Home (Public)</h1>;
const Pricing = () => <h1>Pricing (Public)</h1>;
const Dashboard = () => <h1>Dashboard (Private)</h1>;
const Settings = () => <h1>Settings (Private)</h1>;
const Login = () => <h1>TODO</h1>
function Nav() {
return (
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/pricing">Pricing</Link>
</li>
</ul>
</nav>
);
}
export default function App() {
return (
<div>
<Nav />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/login" element={<Login />} />
</Routes>
</div>
);
}

At this point we’re not doing anything fancy. We’ve successfully mapped the app’s location to a few components, typical React Router stuff.


Now let’s start working on some auth. First, we’ll build out our Login component. The goal of this component is, naturally, to allow the user to login. Because we already have our useAuth Hook, most of the heavy lifting is already done.

import { useNavigate } from 'react-router-dom'
import useAuth from './useAuth'
const Login = () => {
const navigate = useNavigate();
const { login } = useAuth();
const handleLogin = () => {
login().then(() => {
navigate("/dashboard");
});
};
return (
<div>
<h1>Login</h1>
<button onClick={handleLogin}>
Log in
</button>
</div>
);
};

Our (simple) Login component renders a header and a button. When the user clicks on the button, we call login (which we got from our useAuth Hook), then once they’re logged in, using navigate, we send them to their /dashboard.

Programmatically Navigate

If you’re not familar with React Router’s useNavigate Hook or their Navigate component, now might be a good time to check out How to Programmatically Navigate with React Router.


Next, let’s add the ability to logout. Again, we already have our logout method from our useAuth Hook, so this too should simply be adding in some UI. All of the changes will be to our Nav component.

import { useNavigate } from 'react-router-dom'
import useAuth from './useAuth'
function Nav () {
const { authed, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/");
};
return (
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/pricing">Pricing</Link>
</li>
</ul>
{authed && (
<button onClick={handleLogout}>
Logout
</button>
)}
</nav>
);
}

Now the fun part, time to make our /dashboard and /settings routes private so only users who are authenticated can access them.

It would be nice if, just like React Router gave us a Route component, they also gave us a PrivateRoute component which would render the element only if the user was authenticated.

Something like this.

<Routes>
<Route path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<PrivateRoute path="/dashboard" element={<Dashboard />} />
<PrivateRoute path="/settings" element={<Settings />} />
<Route path="/login" element={<Login />} />
</Routes>

Unfortunately, they don’t. However, the good news is there’s nothing stopping us from composing Route into our own PrivateRoute component. This is much more flexible than anything React Router could give us out of the box anyway.

Here are the requirements for our PrivateRoute component.

PrivateRoute Requirements

1) It has the same API as <Route />

2) It checks if the user is authenticated. If they are, it renders a Route. If not, it redirects the user to /login

First, we want PrivateRoute to have the same API as Route. This means it’ll accept a path as well as an element.

function PrivateRoute({ path, element }) {
}

Second, it checks if the user is authenticated. If they are, it renders the Route. If not, it redirects the user to /login.

import { Navigate } from 'react-router-dom'
import useAuth from './useAuth'
function PrivateRoute({ element, path }) {
const { authed } = useAuth();
const ele = authed === true
? element
: <Navigate to="/login" replace />;
return <Route path={path} element={ele} />;
}

Again, there’s nothing fancy going on here. If you’re familiar with JavaScript and React, the solution should feel relatively simple. React Router gives you the routing primitives upon which you can build your app – nothing more and nothing less.


At this point, everything is working fine. When a user who isn’t authenticated tries to go to /dashboard or /settings, they’re redirected to /login. Then once they login, we redirect them back to /dashboard.

Notice an issue here though? It’s small, but it’s a UX anti-pattern. Instead of always redirecting the user to /dashboard, we should redirect them to the route they were originally trying to visit.

For example, if they try to visit /settings but aren’t logged in, after we redirect them and they log in, we should take them back to /settings, not dashboard.

To do this, we’ll first need to update our PrivateRoute component to pass along the path the user was trying to visit when we redirect them to /login. This is simple as Navigate has a state prop to do just this.

function PrivateRoute({ element, path }) {
const { authed } = useAuth();
const ele = authed === true
? element
: <Navigate
to="/login"
replace
state={{ path }}
/>;
return <Route path={path} element={ele} />;
}

Now inside our Login component, we can use React Router’s useLocation Hook to get access to location.state, which will have our path property.

import { useLocation } from 'react-router-dom'
const Login = () => {
const navigate = useNavigate();
const { login } = useAuth();
const { state } = useLocation();
const handleLogin = () => {
login().then(() => {
navigate(state.path || "/dashboard");
});
};
return (
<div>
<h1>Login</h1>
<button onClick={handleLogin}>Log in</button>
</div>
);
};

This is just one example of how you can use React Router to add protected routes to your React application. Because React Router embraces React’s composition model, you can compose it together in any way that makes sense for your app.

Want to learn more?
If you liked this post and want to learn more, check out our free Comprehensive Guide to React Router.

Before you leave

I know, "another newsletter pitch" - but hear me out. Most JavaScript newsletters are terrible. When's the last time you actually looked forward to getting one? Even worse, when's the last time you actually read one rather than just skim it?

We wanted to change that, which is why we created Bytes. The goal was to create a JavaScript newsletter that was both educational and entertaining. 84,338 subscribers and an almost 50% weekly open rate later, it looks like we did it.

Delivered to 84,338 developers every Monday

Avatar for Tyler McGinnis

Tyler McGinnis

CEO of ui.dev. Obsessed with teaching, writing, swimming, biking, and running.

Share this post

Want more react-router?

This is part of our React Router course. You can take the rest of the course by starting a free 3-day trial.