When should you use protected routes?
If you're setting up routing in your React application, there might be a few routes that you don't want
unauthorized users to access. An example of this would be a /profile
route
for an application that requires credential authentication.
Without the implementation of protected routes, anyone can access the "Profile" page simply by typing the path
into the browser's address bar. Ideally, the route should be inaccessible to users without authenticated
credentials. If an unauthorized user tries to access the /profile
route,
they should be redirected back to the /login
route. Luckily, this is super
simple to accomplish, so let's dive right in.
Creating a protected route
For this guide, let's assume I'm creating an application with a "Login", "Home", "Profile", and "Settings" page. With basic routing set up, it should look something like this:
// App.js
import { AuthContextProvider } from "./contexts/auth-context";
import LogIn from "./pages/LogIn";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
import Settings from "./pages/Settings";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const App = () => {
return (
<div>
<AuthContextProvider>
<BrowserRouter>
<Routes>
<Route path="login" element={<LogIn />} />
<Route path="/" element={<Home />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Routes>
</BrowserRouter>
</AuthContextProvider>
</div>
);
};
export default App;
...each component has its own route, and every route is currently accessible to anyone.
The AuthContextProvider
serves as a wrapper component which holds the application's
context, which isn't required or important for this guide — it just detects whether the user is logged in or not. My goal is
to make the /
, /profile
, and
/settings
routes available only to authenticated users.
To do this, I need to create a wrapper component that'll hold the routes I want to protect. I'll name this component
PrivateRoutes.js
and set everything up:
// PrivateRoutes.js
import { useAuth } from "../contexts/auth-context";
import { Outlet, Navigate } from "react-router-dom";
const PrivateRoutes = () => {
const { isLoggedIn } = useAuth();
return isLoggedIn ? <Outlet /> : <Navigate to="login" />;
};
export default PrivateRoutes;
...useAuth
is imported, which simply has the isLoggedIn
state that
holds a Boolean. After that, the Outlet
and Navigate
components are imported. The Outlet
component is used in parent route elements to
render child route elements. The Navigate
component changes the current location of
the application. The ternary operation inside PrivateRoutes
basically says, "If
the user is logged in, render all the child components inside the wrapper; if the user isn't logged in, redirect them
to the 'Login' page if they try to access any of those child components."
Now that the rules for the protected route wrapper component are written, it can be imported and used:
// App.js
import { AuthContextProvider } from "./contexts/auth-context";
import LogIn from "./pages/LogIn";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
import Settings from "./pages/Settings";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PrivateRoutes from "./components/PrivateRoutes";
const App = () => {
return (
<div>
<AuthContextProvider>
<BrowserRouter>
<Routes>
<Route path="login" element={<LogIn />} />
<Route element={<PrivateRoutes />}>
<Route path="/" element={<Home />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
</AuthContextProvider>
</div>
);
};
export default App;
...the PrivateRoutes
component is used as a wrapper around the routes that require
authentication. This works perfectly, but now there's an issue: even if the user is logged in, they can still
access the /login
path. Luckily, this can be solved by creating another wrapper
component similar to the one I just created:
// DisableAuthenticateRoute.js
import { useAuth } from "../contexts/auth-context";
import { Outlet, Navigate } from "react-router-dom";
const DisableAuthenticateRoute = () => {
const { isLoggedIn } = useAuth();
return isLoggedIn ? <Navigate to="/" /> : <Outlet />;
};
export default DisableAuthenticateRoute;
...this time, the ternary operation says, "If the user is logged in, redirect them to the 'Home' page if they try to access the 'Login' page; if the user isn't logged in, render all the child components inside the wrapper."
With everything set up and complete, the final routing looks something like this:
// App.js
import { AuthContextProvider } from "./contexts/auth-context";
import LogIn from "./pages/LogIn";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
import Settings from "./pages/Settings";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PrivateRoutes from "./components/PrivateRoutes";
import DisableAuthenticateRoute from "./components/DisableAuthenticateRoute";
const App = () => {
return (
<div>
<AuthContextProvider>
<BrowserRouter>
<Routes>
<Route element={<DisableAuthenticateRoute />}>
<Route path="login" element={<LogIn />} />
</Route>
<Route element={<PrivateRoutes />}>
<Route path="/" element={<Home />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
</AuthContextProvider>
</div>
);
};
export default App;
...all done! Unauthorized users will now be redirected to the /login
path until they
log in, and authorized users won't be able to access that path unless they log out.