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. 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. 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? We wanted to change that.
$ npm install react - router - guards
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);}
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> );}
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> );}
<Routes> <Route path="/" element={<Home />} /> <Route path="/pricing" element={<Pricing />} /> <Route path="/dashboard" element={ <RequireAuth> <Dashboard /> </RequireAuth> } /> <Route path="/settings" element={ <RequireAuth> <Settings /> </RequireAuth> } /> <Route path="/login" element={<Login />} /></Routes>
I was trying to implement authenticated routes but found that React Router 4 now prevents this from working: I was looking for a solution where my main router file had everything it needed to authenticate the routes. No nested component needed or complicated if else's. Below is my approach In 2022 the render prop of the Route component is for legacy use according to the react-router-dom documentation is not even working anymore in V5 and in V6 was removed. If you're still confused, I wrote this post that may help - Protected routes and authentication with React Router v4
You're going to want to use the Redirect
component. There's a few different approaches to this problem. Here's one I like, have a PrivateRoute component that takes in an authed
prop and then renders based on that props.
function PrivateRoute ({component: Component, authed, ...rest}) {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
/>
)
}
Now your Route
s can look something like this
<Route path='/' exact component={Home} />
<Route path='/login' component={Login} />
<Route path='/register' component={Register} />
<PrivateRoute authed={this.state.authed} path='/dashboard' component={Dashboard} />
This works instead:
const RequireAuth: FC<{ children: React.ReactElement }> = ({ children }) => {
const userIsLogged = useLoginStatus(); // Your hook to get login status
if (!userIsLogged) {
return <LoginPage />;
}
return children;
};
React-router will handle our routing, i.e switching from one page to another within the web application. If you would be interested in implementing an automatic logout feature after some seconds of user inactivity, read this next article I wrote - Implementing AutoLogout Feature in Web Applications (React) For this tutorial, I'll be showing how to set up an authentication route and protect other routes from been accessed by unauthorized users. Yes, you can use react-guards to check if user is logged in or not. Then implement the routing
const handleInputChange = (e) => {
setUserData((prevState) => {
return {
...prevState,
[e.target.name]: e.target.value,
};
});
};
const handleSubmit = (e) => {
e.preventDefault();
//if username or password field is empty, return error message
if (userData.username === "" || userData.password === "") {
setErrorMessage((prevState) => ({
value: "Empty username/password field",
}));
} else if (
userData.username.toLowerCase() === "admin" &&
userData.password === "123456"
) {
//Signin Success
localStorage.setItem("isAuthenticated", "true");
window.location.pathname = "/";
} else {
//If credentials entered is invalid
setErrorMessage((prevState) => ({
value: "Invalid username/password"
}));
return;
}
};
import React from "react";
import { Redirect, Route } from "react-router-dom";
function ProtectedRoute({ component: Component, ...restOfProps }) {
const isAuthenticated = localStorage.getItem("isAuthenticated");
console.log("this", isAuthenticated);
return (
<Route
{...restOfProps}
render={(props) =>
isAuthenticated ? <Component {...props} /> : <Redirect to="/signin" />
}
/>
);
}
export default ProtectedRoute;
Before creating the protected route (also referred to as a private route), let’s create a custom hook that will handle the authenticated user’s state using the Context API and useContext hook: 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. This tutorial will demonstrate how to create protected routes and add authentication with React Router v6. Instead, we can use the React Router v6 nested route feature to wrap all the protected routes in a single layout.
Open up the terminal and create a new React project by running the following command:
> npx create - react - app ReactRouterAuthDemo > 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/index.js
file.
Import BrowserRouter
from react-router-dom
and then wrap the <App />
component with <BrowserRouter />
, like so:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
<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 <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>
);
Protected routes are routes that require user authorization in order to be accessed. When you are building a web application, some of your routes may require authentication, which means restricting user access to certain pages or you having your whole application behind a login. React router is a great way to go when it comes to routing, but you don’t really have the option to protect routes from being accessed by anyone. Luckily the solution to this is really simple and straightforward. To summarise: Even though how your protected routes work in React will be obfuscated, the way your client application works can be reverse-engineered. This is why it is essential that everything that involves authentication or authorization should be backed up by a server-side implementation.
{
"name": "auth",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
+"react-router-dom": "^5.1.2",
"react-scripts": "3.3.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
import React from 'react';
import ReactDOM from 'react-dom';
+ import { Route, BrowserRouter, Switch } from 'react-router-dom';
import './index.css';
+ import Login from './Login';
+ import Dashboard from './Dashboard';
+ import Settings from './Settings';
+ import ProtectedRoute from './ProtectedRoute';
import * as serviceWorker from './serviceWorker';
+ ReactDOM.render((
+ <BrowserRouter>
+ <Switch>
+ <Route path="/login" component={Login} />
+ <ProtectedRoute exact={true} path="/" component={Dashboard} />
+ <ProtectedRoute path="/settings" component={Settings} />
+ <ProtectedRoute component={Dashboard} />
+ </Switch>
+ </BrowserRouter>
+ ), document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
import React from 'react'
import { Redirect } from 'react-router-dom'
class ProtectedRoute extends React.Component {
render() {
const Component = this.props.component;
const isAuthenticated = ???;
return isAuthenticated ? (
<Component />
) : (
<Redirect to={{ pathname: '/login' }} />
);
}
}
export default ProtectedRoute;
A React Router tutorial which teaches you how to perform a Redirect in React Router 6 . The code for this React Router v6 tutorial can be found over here . In order to get you started, create a new… A React Router tutorial which teaches you how to use Nested Routes with React Router 6 . The code for this React Router v6 tutorial can be found over here . In order to get you started, create a new… A React Router tutorial which teaches you how to use Authentication in React Router 6. The code for this React Router v6 tutorial can be found over here. In order to get you started, create a new React project (e.g. create-react-app). Afterward, install React Router and read the following React Router tutorial to get yourself aligned to what follows next.
We will start off with a minimal React project that uses React Router to navigate a user from one page to another page. In the following function component, we have matching Link and Route components from React Router for the home/
and dashboard/
routes. Furthermore, we have a so-called Index Route loaded with the Home component and a so-called No Match Route loaded with the NoMatch component. Both act as fallback routes:
import { Routes, Route, Link } from 'react-router-dom';
const App = () => { return ( <> <h1>React Router</h1>
<Navigation />
<Routes> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="dashboard" element={<Dashboard />} /> <Route path="*" element={<NoMatch />} /> </Routes> </> );};
const Navigation = () => { return ( <nav> <NavLink to="/home">Home</NavLink> <NavLink to="/dashboard">Dashboard</NavLink> </nav> );};
So whether you are authenticating against a REST API, a GraphQL API, or a backend-as-a-service such as Firebase is up to you. What matters in the end is that the authentication API returns your frontend a token (e.g. JWT) after a successful authentication and React Router will take over from there (e.g. redirecting the user after a login).
We will use a fake API to mock the authentication to a backend. This fake API is just a function which resolves a string from a promise with a delay. However, if you have a backend which supports authentication, you can hit the backend API instead and don't need to implement the following function in your frontend:
const fakeAuth = () => new Promise((resolve) => {
setTimeout(() => resolve('2342f2f1d131rf12'), 250);
});
But let's start simple. In the previous example, we created two routes for a Home and a Dashboard component. These components may be implemented the following way and already indicate whether they can be accessed by a authorized user:
const Home = () => { return ( <> <h2>Home (Public)</h2> </> );};
const Dashboard = () => { return ( <> <h2>Dashboard (Protected)</h2> </> );};
In a real world scenario, you would use a bunch of HTML form elements to catch a user's email/password combination and pass it up via the callback handler when a user submits the form. However, in order to keep it simple, we are using only a button here.
Next, up in the parent component, we create the actual event handler which is passed down to the Home component as callback handler via React props and which is called whenever a user clicks the button in the Home component. Within the callback handler we execute the fake API which returns a token for us. Again, if you have your own backend with an authentication API, you can authenticate against the real backend instead:
const App = () => { const [token, setToken] = React.useState(null);
const handleLogin = async () => { const token = await fakeAuth();
setToken(token); };
return ( <> <h1>React Router</h1>
<Navigation />
<Routes> <Route index element={<Home onLogin={handleLogin} />} /> <Route path="home" element={<Home onLogin={handleLogin} />} /> <Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} /> </Routes> </> );};
Furthermore, we used React's useState Hook to store the token as component state. The token itself is a representation of the authentication user. In a real world scenario, you may have a JWT token which encapsulates information (e.g. username, email) of the user.
An application with a login needs to have an equivalent logout as well. In our case, the logout will be initiated in the top-level Navigation component, but feel free to put it anywhere you want. Within the new callback handler that is passed to the Navigation component, we will only reset the token to null
in the component's state when a users signs out from the application:
const App = () => { const [token, setToken] = React.useState(null);
const handleLogin = async () => { const token = await fakeAuth();
setToken(token); };
const handleLogout = () => { setToken(null); };
return ( <> <h1>React Router</h1>
<Navigation token={token} onLogout={handleLogout} />
... </> );};