Design Converter
Education
Last updated on Mar 3, 2025
•16 mins read
Last updated on Mar 3, 2025
•16 mins read
How do you keep private pages secure in a React app? 🔒
Public pages like a home or login screen should be open to everyone. But what about a dashboard or user profile? You wouldn't want just anyone to access them. This is where react-router protected routes help.
They block unauthorized users from viewing sensitive pages. Only authenticated users can enter. Without this setup, private data could be at risk.
Let’s go over how to add this security to your React app.
React Router is the go-to library for handling navigation in a React application. It allows you to define multiple routes in React, creating a seamless user experience. Whether you're setting up a simple home page, a complex dashboard page, or nested routes, React Router makes it easy to manage navigation.
In modern applications, managing route paths effectively is essential for both UX and security. The library react-router-dom provides all the tools needed to define, manage, and secure routes within your React app.
At its core, a route is a mapping between a URL and a React component. The route path defines which component should be rendered based on the URL. A basic setup using react-router-dom looks like this:
1import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2import HomePage from "./pages/HomePage"; 3import Dashboard from "./pages/Dashboard"; 4import LoginPage from "./pages/LoginPage"; 5 6function App() { 7 return ( 8 <Router> 9 <Routes> 10 <Route path="/" element={<HomePage />} /> 11 <Route path="/login" element={<LoginPage />} /> 12 <Route path="/dashboard" element={<Dashboard />} /> 13 </Routes> 14 </Router> 15 ); 16} 17 18export default App;
Here, we define multiple route paths, including a home page, a dashboard page, and a login page. However, right now, nothing is stopping an unauthenticated user from accessing the dashboard page directly.
This is where protected routes come into play. By implementing a ProtectedRoute component, we can ensure that only authenticated users can access certain pages.
A protected route works by checking the authentication status of a user before rendering a route component. If the user is authenticated, they proceed; otherwise, they are redirected to a public route like the login page.
Before you can define routes in React, you need to install react-router-dom, which is the core package for handling navigation in a React application. This package allows you to set up route paths, manage nested routes, and create both public routes and protected routes.
To install react-router-dom, use the following command:
1npm install react-router-dom
Once installed, you need to configure React Router in your React app. This involves wrapping your application inside the BrowserRouter component.
Open your App.js or App.tsx file and set up the router dom like this:
1import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2import HomePage from "./pages/HomePage"; 3import Dashboard from "./pages/Dashboard"; 4import LoginPage from "./pages/LoginPage"; 5 6function App() { 7 return ( 8 <Router> 9 <Routes> 10 <Route path="/" element={<HomePage />} /> 11 <Route path="/login" element={<LoginPage />} /> 12 <Route path="/dashboard" element={<Dashboard />} /> 13 </Routes> 14 </Router> 15 ); 16} 17 18export default App;
Explanation:
• The Router component (alias for BrowserRouter) wraps the entire React application to enable route paths.
• The Routes component acts as a container for all defined route elements.
• The Route component maps a specific route path to a React component.
• Users can now navigate between the home page, dashboard page, and login page by changing the URL.
Now that React Router is configured, let’s create the basic components that will be used in the route elements.
Inside the pages folder, create a file named HomePage.js:
1import React from "react"; 2 3function HomePage() { 4 return <h1>Welcome to the Home Page</h1>; 5} 6 7export default HomePage;
Create another file named LoginPage.js:
1import React from "react"; 2 3function LoginPage() { 4 return <h1>Please log in to continue</h1>; 5} 6 7export default LoginPage;
The dashboard page should be a protected route, which means only authenticated users can access it. For now, we'll just create the page and handle protection in the next section.
Create a file named Dashboard.js:
1import React from "react"; 2 3function Dashboard() { 4 return <h1>Welcome to Your Dashboard</h1>; 5} 6 7export default Dashboard;
Now, if you run your React app, you can visit:
• /
for the home page
• /login
for the login page
• /dashboard
for the dashboard page
However, the dashboard page is currently accessible to anyone. To fix this, we need to implement protected routes using a ProtectedRoute component. This ensures that only authorized users with valid authentication status can access the protected page.
In a React application, not all pages should be accessible to every user. Some pages, such as a dashboard page, user settings, or admin panel, should only be available to authenticated users. This is where protected routes come into play.
A protected route ensures that a user must complete the authentication process before accessing certain route paths. If the user is authenticated, they can access the protected page. If not, they are redirected to a public route, such as a login page.
When a user attempts to access a protected route, the application checks their authentication status.
If the user is authenticated, the route component is rendered.
If the user is not authenticated, they are redirected to the login page.
Once authenticated, they can access protected routes and other nested routes within the React application.
Now, let’s create a ProtectedRoute component to handle this logic efficiently.
To implement protected routes, create a new file ProtectedRoute.js inside a components folder:
1import React from "react"; 2import { Navigate } from "react-router-dom"; 3 4const ProtectedRoute = ({ user, children }) => { 5 if (!user) { 6 return <Navigate to="/login" replace />; 7 } 8 return children; 9}; 10 11export default ProtectedRoute;
Explanation:
• The ProtectedRoute component takes in a user prop, which represents the authentication status of the user.
• If the user is not authenticated, the Navigate component from react-router-dom redirects them to the login page.
• If the user is authenticated, the route element (the protected page) is rendered.
Now, update your App.js to use the ProtectedRoute for private routes like the dashboard page.
1import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2import HomePage from "./pages/HomePage"; 3import Dashboard from "./pages/Dashboard"; 4import LoginPage from "./pages/LoginPage"; 5import ProtectedRoute from "./components/ProtectedRoute"; 6import { useState } from "react"; 7 8function App() { 9 const [user, setUser] = useState(null); // Simulating authentication status 10 11 return ( 12 <Router> 13 <Routes> 14 <Route path="/" element={<HomePage />} /> 15 <Route path="/login" element={<LoginPage />} /> 16 <Route 17 path="/dashboard" 18 element={ 19 <ProtectedRoute user={user}> 20 <Dashboard /> 21 </ProtectedRoute> 22 } 23 /> 24 </Routes> 25 </Router> 26 ); 27} 28 29export default App;
Explanation:
• We use useState to manage the user authentication state. In a real-world React application, this would come from a global state (like Redux) or local storage.
• The dashboard page is wrapped inside the ProtectedRoute component, ensuring only authenticated users can access it.
• If an unauthenticated user tries to access /dashboard*
, they are redirected to the login page.
Sometimes, after a user logs in, you may want to redirect them to the page they originally tried to visit. To achieve this, modify the ProtectedRoute component to store the route path they were attempting to access before redirecting them.
Update ProtectedRoute.js like this:
1import React from "react"; 2import { Navigate, useLocation } from "react-router-dom"; 3 4const ProtectedRoute = ({ user, children }) => { 5 const location = useLocation(); 6 7 if (!user) { 8 return <Navigate to="/login" state={{ from: location }} replace />; 9 } 10 return children; 11}; 12 13export default ProtectedRoute;
Explanation:
• The useLocation hook captures the current route path.
• If a user is not authenticated, they are redirected to /login
, but their original route path is saved in state.
• After login, they can be redirected back to their intended page.
Modify LoginPage.js to check if the user was redirected from another page:
1import React from "react"; 2import { useNavigate, useLocation } from "react-router-dom"; 3 4function LoginPage({ setUser }) { 5 const navigate = useNavigate(); 6 const location = useLocation(); 7 const from = location.state?.from?.pathname || "/dashboard"; 8 9 const handleLogin = () => { 10 setUser(true); // Simulating authentication 11 navigate(from, { replace: true }); // Redirecting back to the intended route 12 }; 13 14 return ( 15 <div> 16 <h1>Login Page</h1> 17 <button onClick={handleLogin}>Login</button> 18 </div> 19 ); 20} 21 22export default LoginPage;
Explanation:
• The useNavigate hook helps redirect the user after login.
• If they were redirected from a protected route, the original route path is extracted from location.state.
• After successful login, they are redirected back to the intended route path instead of always going to the dashboard page.
In a React application, restricting access based on authentication alone is not always enough. Some protected routes may require specific roles, such as "Admin," "User," or "Editor." This is where role-based access control (RBAC) comes into play.
For example:
• A dashboard page should be accessible to both regular users and admins.
• An admin panel should be accessible only to users with an "Admin" role.
• A settings page might be accessible to both "Admin" and "Editor" roles but restricted for regular users.
With react-router-dom, we can modify our ProtectedRoute component to check both authentication and user roles before allowing access to certain route paths.
To implement role-based permissions, we need to store the user's role in the React application. This can be managed using useState, local storage, or global state management tools like Redux or Context API.
For this example, let's modify our App.js file to include user roles:
1import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2import HomePage from "./pages/HomePage"; 3import Dashboard from "./pages/Dashboard"; 4import LoginPage from "./pages/LoginPage"; 5import AdminPage from "./pages/AdminPage"; 6import ProtectedRoute from "./components/ProtectedRoute"; 7import { useState } from "react"; 8 9function App() { 10 const [user, setUser] = useState(null); 11 12 return ( 13 <Router> 14 <Routes> 15 <Route path="/" element={<HomePage />} /> 16 <Route path="/login" element={<LoginPage setUser={setUser} />} /> 17 <Route 18 path="/dashboard" 19 element={ 20 <ProtectedRoute user={user}> 21 <Dashboard /> 22 </ProtectedRoute> 23 } 24 /> 25 <Route 26 path="/admin" 27 element={ 28 <ProtectedRoute user={user} allowedRoles={["admin"]}> 29 <AdminPage /> 30 </ProtectedRoute> 31 } 32 /> 33 </Routes> 34 </Router> 35 ); 36} 37 38export default App;
Explanation:
• The user state contains authentication and role information.
• The dashboard page is accessible to all authenticated users.
• The admin panel (/admin
) is protected using role-based permissions and is restricted to users with the "Admin" role.
Now, let's modify the ProtectedRoute component to check for user roles before granting access:
1import React from "react"; 2import { Navigate, useLocation } from "react-router-dom"; 3 4const ProtectedRoute = ({ user, allowedRoles, children }) => { 5 const location = useLocation(); 6 7 if (!user) { 8 return <Navigate to="/login" state={{ from: location }} replace />; 9 } 10 11 if (allowedRoles && !allowedRoles.includes(user.role)) { 12 return <Navigate to="/" replace />; 13 } 14 15 return children; 16}; 17 18export default ProtectedRoute;
Explanation:
• If the user is not authenticated, they are redirected to the login page.
• If allowedRoles is defined and the user role is not included, they are redirected to the home page.
• If the user has the correct role, they can access the requested route element.
Sometimes, you may need to hide or show components based on user roles, even within a single route component. For example, an admin might see an "Admin Settings" button on the dashboard page, while a regular user does not.
Let's update Dashboard.js to dynamically render UI elements based on the user role:
1import React from "react"; 2 3const Dashboard = ({ user }) => { 4 return ( 5 <div> 6 <h1>Welcome to Your Dashboard</h1> 7 {user?.role === "admin" && <button>Go to Admin Panel</button>} 8 </div> 9 ); 10}; 11 12export default Dashboard;
Explanation:
• If the user role is "admin," an Admin Panel button is displayed.
• Regular users will not see this button.
We need to update the LoginPage.js to set both authentication and user roles upon login:
1import React from "react"; 2import { useNavigate, useLocation } from "react-router-dom"; 3 4function LoginPage({ setUser }) { 5 const navigate = useNavigate(); 6 const location = useLocation(); 7 const from = location.state?.from?.pathname || "/dashboard"; 8 9 const handleLogin = (role) => { 10 const userData = { username: "JohnDoe", role }; // Simulating a logged-in user 11 setUser(userData); // Set the user with role 12 localStorage.setItem("user", JSON.stringify(userData)); // Store in local storage 13 navigate(from, { replace: true }); 14 }; 15 16 return ( 17 <div> 18 <h1>Login Page</h1> 19 <button onClick={() => handleLogin("user")}>Login as User</button> 20 <button onClick={() => handleLogin("admin")}>Login as Admin</button> 21 </div> 22 ); 23} 24 25export default LoginPage;
Explanation:
• Users can log in as either a regular user or an admin.
• The authentication status and user role are stored in local storage so they persist across refreshes.
Since local storage retains data after a page refresh, we should retrieve the user information when the app loads. Modify App.js like this:
1import { useEffect, useState } from "react"; 2 3function App() { 4 const [user, setUser] = useState(null); 5 6 useEffect(() => { 7 const storedUser = localStorage.getItem("user"); 8 if (storedUser) { 9 setUser(JSON.parse(storedUser)); 10 } 11 }, []); 12 13 return ( 14 // Routes setup 15 ); 16}
Explanation:
• The useEffect hook loads the user authentication data from local storage when the React app starts.
• This ensures users remain logged in even after refreshing the page.
In a React application, loading all protected routes at once can slow down performance. Instead, we can use lazy loading to load components only when needed. This reduces the initial bundle size and improves performance, especially for large applications.
To enable lazy loading, use React.lazy()
and Suspense when defining route elements in react-router-dom.
Modify App.js like this:
1import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2import { Suspense, lazy, useState, useEffect } from "react"; 3import ProtectedRoute from "./components/ProtectedRoute"; 4 5const HomePage = lazy(() => import("./pages/HomePage")); 6const Dashboard = lazy(() => import("./pages/Dashboard")); 7const LoginPage = lazy(() => import("./pages/LoginPage")); 8const AdminPage = lazy(() => import("./pages/AdminPage")); 9 10function App() { 11 const [user, setUser] = useState(null); 12 13 useEffect(() => { 14 const storedUser = localStorage.getItem("user"); 15 if (storedUser) { 16 setUser(JSON.parse(storedUser)); 17 } 18 }, []); 19 20 return ( 21 <Router> 22 <Suspense fallback={<div>Loading...</div>}> 23 <Routes> 24 <Route path="/" element={<HomePage />} /> 25 <Route path="/login" element={<LoginPage setUser={setUser} />} /> 26 <Route 27 path="/dashboard" 28 element={ 29 <ProtectedRoute user={user}> 30 <Dashboard /> 31 </ProtectedRoute> 32 } 33 /> 34 <Route 35 path="/admin" 36 element={ 37 <ProtectedRoute user={user} allowedRoles={["admin"]}> 38 <AdminPage /> 39 </ProtectedRoute> 40 } 41 /> 42 </Routes> 43 </Suspense> 44 </Router> 45 ); 46} 47 48export default App;
Explanation:
• lazy(() => import("./pages/Dashboard"))
ensures that the dashboard page is loaded only when required.
• Suspense provides a fallback UI (e.g., "Loading...") while components are being loaded.
• This improves performance by preventing unnecessary route elements from being loaded upfront.
While protected routes in react-router-dom secure the frontend, they do not prevent unauthorized users from accessing data via API calls. To strengthen security, always validate the authentication status and user role on the backend.
When a user logs in, the backend issues a JWT (JSON Web Token).
The token is stored in local storage or httpOnly cookies.
When accessing a protected route, the frontend sends the token to the backend for validation.
If the token is valid, the backend returns the requested data; otherwise, it rejects the request.
Example: Backend Route Protection with Express.js
In an Express.js backend, you can protect API routes like this:
1const jwt = require("jsonwebtoken"); 2 3const authenticateUser = (req, res, next) => { 4 const token = req.headers.authorization?.split(" ")[1]; 5 if (!token) { 6 return res.status(401).json({ message: "Unauthorized" }); 7 } 8 9 try { 10 const decoded = jwt.verify(token, process.env.JWT_SECRET); 11 req.user = decoded; 12 next(); 13 } catch (error) { 14 return res.status(403).json({ message: "Invalid token" }); 15 } 16}; 17 18// Example of a protected API route 19app.get("/api/protected-data", authenticateUser, (req, res) => { 20 res.json({ message: "You have access to this data", user: req.user }); 21});
Explanation:
• The authenticateUser middleware checks for a valid JWT token in the request headers.
• If the token is valid, the request proceeds; otherwise, access is denied.
• This ensures that even if an unauthorized user bypasses the frontend, they still cannot access sensitive data.
To ensure optimal performance and security in react router protected routes, follow these best practices:
• Implement role-based permissions in the ProtectedRoute component.
• Store the user’s role in local storage and validate it before rendering route elements.
• Example: Restrict access to an admin panel based on user roles.
• Instead of storing JWTs in local storage, use httpOnly cookies to prevent XSS attacks.
• Example:
1res.cookie("token", jwtToken, { httpOnly: true, secure: true });
• Always validate authentication status and user role on the backend.
• Use jwt.verify() to confirm that a user has a valid session.
• Use React.lazy() and Suspense to defer loading large components.
• This prevents unnecessary rendering of protected components in nested routes.
• Do not rely on frontend React state alone for role validation.
• Always verify roles on the backend before granting access to sensitive actions.
• Store API keys, JWT secrets, and other sensitive data in .env files instead of hardcoding them.
1JWT_SECRET=mysecretkey
• Ensure users can log out by clearing both local storage and authentication tokens.
1const handleLogout = () => { 2 localStorage.removeItem("user"); 3 navigate("/login"); 4};
• When an unauthorized user tries to access a protected page, redirect them to /login
or the home page.
1return <Navigate to="/login" replace />;
Building react-router protected routes helps secure your app while improving user experience. With authentication, role-based access, and lazy loading, you can control access and boost performance.
Take it further by exploring JWT authentication, secure cookies, and API-based authorization. Keep testing different strategies and stay updated with best practices. A well-protected route system lays the groundwork for a secure and scalable React app.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.