React Public, Protected, and Private Routes: A Comprehensive Guide
In certain applications, you might need to implement role-based authorization for your users to access certain pages and be restricted to others. For instance, in an e-commerce application, your users could sign up as a seller or a buyer, and based on their role, you give them access to certain pages in the application. Each page not accessible to a user due to their role is hidden in a private route, and every other page accessible to the users irrespective of their roles is a public route. Aside from private and public routes, there are also protected routes. To access the pages hidden in a protected route, your users must be authenticated—signed up or logged in.
In a multiple-role-based application, you need to implement these routes to have certain accesses and restrictions in place for your users. Without them, your application will be disorganized and confusing for your users and you, especially when accessing users' data.
React Router, the routing library built for React applications, does not have these pre-installed features, so you need to write special configurations for them in your application. In this article, you’ll learn how to create these three special routes for your applications and implement role-based authorization in your React apps through them.
Prerequisites
Although this is a straightforward article, you need to have a good understanding of:
- React
- React Router
- Authorization and Authentication.
Nevertheless, I’ll briefly introduce the mentioned libraries.
Brief Introduction to React, React Router, Authorization, and Authentication
React is a popular JavaScript frontend library built by Facebook to help developers easily start up a web application. It’s a Single Page Application (SPA) library which renders your app in a single HTML file rather than having multiple HTML files for all your application’s pages. React allows you to break down frequently occurring aspects of your app into reusable components, preventing unnecessary repetitions which tend to increase your file size and slow down page load time. In React applications, React Router is used to enable easy navigation across pages. Rather than moving from the home page to the products page with an anchor tag that links both pages, you could use a Link component from the React Router Dom library to achieve the same result faster. In addition to being fast, using React Router Link prevents page refresh, which the anchor tag enables, thereby preserving your application states which could be wiped away when the page is refreshed in the case of an anchor tag.
Authorization and authentication, though appear to have similar meanings, are different. Authentication is the validation of your entity while attempting entrance to a web application, while authorization is the access to features upon your entrance. For instance, you are checked at the front desk of a hotel after booking. The front desk clerk checks to confirm that you’re the one who booked a suite in the hotel by asking for a form of proof which could be a passcode—that is authentication. Upon identity verification at the front desk, you get a key or a card to access the room—authorization. Also, based on the suite type you paid for, you’re entitled to certain hospitality features and restricted from others—role-based authorization.
With the prerequisite terms cleared, let’s proceed to implementing the routes.
Route Definition: Explaining the routes in detail
Before we dive into protected routes, it's important to understand the distinction between different routes in a React application. Here's a table that clarifies the differences between public, private, and protected routes:

Understanding these distinctions will help you implement the appropriate routing strategy for your application.
Protected Route
Any page that you cannot access without authentication is a protected route. To implement a protected route in your application, you need to monitor the logged-in state of your user to know when to allow the user to access the page or redirect the user to the login page. The common page access authorization you get upon authentication is the token. The token is similar to the room key or card you get in a hotel upon identity verification, and in a web application, it is stored in the localStorage
in most web applications. However, we'll use HTTP-only cookies to store authentication tokens instead of localStorage
for enhanced security. Opposed to using localStorage
, this approach helps protect against XSS attacks. The presence of the token in the browser cookies tells you that your user is authenticated, and the absence tells you the reverse—the user has logged out of the application. With this detail, we can implement a protected route component.
First and foremost, let’s create a React app and install React Router.
npm create vite@latest authorization-routes -- --template //vite
npm i react-router-dom // React Router
Cd into your app’s directory and open it in your code editor.
cd authorization-routes
code .
Encapsulate your application’s route in React router by wrapping the BrowserRouter
component from react-router-dom
around the AllRoutes.jsx
file.
App.jsx
import { BrowserRouter } from "react-router-dom";
export default function App() {
return (
<BrowserRouter>
<AllRoutes />
</BroswerRouter>
)
}
Then, create a useAuth hook to handle the users’ logged-in state and roles.
import { useState, useEffect } from 'react';
export function useAuth() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userRole, setUserRole] = useState(null);
useEffect(() => {
const checkAuthStatus = async () => {
try {
// Note: Ensure this endpoint is implemented on your server
// It should validate the token (e.g., check expiration, decode payload)
const response = await fetch('/api/auth/status', { credentials: 'include' });
if (response.ok) {
const data = await response.json();
setIsLoggedIn(true);
setUserRole(data.role);
} else {
// If the response is not OK, assume the user is not logged in
setIsLoggedIn(false);
setUserRole(null);
}
} catch (error) {
console.error('Error checking auth status:', error);
// In case of an error, assume the user is not logged in
setIsLoggedIn(false);
setUserRole(null);
}
};
checkAuthStatus();
// You might want to add a cleanup function or re-fetch logic here
// depending on your application's needs
}, []);
return { isLoggedIn, userRole };
}
The useAuth hook is a custom React hook designed to manage the authentication state in our application.
Now, set up your AllRoutes.jsx
page and implement the ProtectedRoute.
AllRoutes.jsx
import { Routes, Route, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./hooks/useAuth";
import LoginPage from "./pages/Auth/LoginPage";
import SignupPage from "./pages/Auth/SignupPage";
import DashboardPage from "./pages/Dashboard";
import ProfilePage from "./pages/Dashboard/ProfilePage";
export default function AllRoutes() {
const location = useLocation();
const { isLoggedIn } = useAuth();
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/sign-up" element={<SignupPage />} />
<Route path="/user" element={<ProtectedRoute />}>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="profile" element={<ProfilePage />} />
{/* Other protected routes */}
</Route>
</Routes>
)
}
Just as stated earlier, the protected route component follows a simple principle. It checks if a user is currently authenticated based on the presence of the token in the localStorage
API. If the token is present, the user is authorized to access every route without role-based authorization. If not, the user is redirected to the login page to get authenticated.
The replace
prop in the Navigate
component ensures that the user cannot access the last authenticated page by clicking the back button on the browser.
Private Route
The Private component follows a similar paradigm to the protected route but with an extra layer of checks. In addition to checking if the user is authenticated, it checks for the user role before granting them access to the page.
AllRoutes.jsx
import { Routes, Route, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./hooks/useAuth";
import LoginPage from "./pages/Auth/LoginPage";
import SignupPage from "./pages/Auth/SignupPage";
import DashboardPage from "./pages/Dashboard";
import ProfilePage from "./pages/Dashboard/ProfilePage";
import UnauthorizedPage from "./pages/UnauthorizedPage";
import NotFoundPage from "./pages/NotFoundPage";
export default function AllRoutes() {
const location = useLocation();
const { isLoggedIn, userRole } = useAuth();
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
const PrivateRoute = ({ children, allowedRoles = [] }) => {
const { isLoggedIn, userRole } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (allowedRoles.length && !allowedRoles.includes(userRole)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/sign-up" element={<SignupPage />} />
{/* Protected routes */}
<Route path={`/${userRole}`} element={<ProtectedRoute />}>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="profile" element={<ProfilePage />} />
</Route>
{/* Buyer's routes */}
<Route path={`/${userRole}`} element={<PrivateRoute
allowedRoles={["buyer"]} />}>
<Route path="cart" element={<CartPage />} />
<Route path="wishlist" element={<WishlistPage />} />
</Route>
{/* Seller's routes */}
<Route path={`/${userRole}`} element={<PrivateRoute
allowedRole={["seller"]} />}>
<Route path="post-product" element={<PostProduct />} />
<Route path="all-products" element={<AllProductsPage/>} />
</Route>
{/* Error handling routes */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
)
}
An extra layer of scrutiny has been added to check the roles and give users access to the pages. The Private route takes 2 props—the children (the pages to be returned if the check is passed) and the role. The component checks if the user is logged in and the role passed matches the role returned by the useAuth hook. If the checks return true
, the user gets access to the page; if not, the user is redirected to the login page.
Public Route
Although the public routes sound like those that should not need a special component, but when you realize how funny your users might be, you see a need to implement one. Logged-in users could decide to clear their current path in the browser and replace it with “/” to get to the home page rather than log out, and if your application allows such, it’s a terrible user experience practice. A public route component prevents logged-in users from accessing unauthenticated pages in the application by redirecting them to the current page they are on until they log out.
The Public route component contrasts the Protected and Private route components.
AllRoutes.jsx
import { Routes, Route, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./hooks/useAuth";
import LoginPage from "./pages/Auth/LoginPage";
import SignupPage from "./pages/Auth/SignupPage";
import DashboardPage from "./pages/Dashboard";
import ProfilePage from "./pages/Dashboard/ProfilePage";
export default function AllRoutes() {
const location = useLocation();
const { isLoggedIn, userRole } = useAuth();
// Protected Route
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
// Private Route
const PrivateRoute = ({ children, allowedRoles = [] }) => {
const { isLoggedIn, userRole } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (allowedRoles.length && !allowedRoles.includes(userRole)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
// Public Route
const PublicRoute = ({ children }) => {
const { isLoggedIn, userRole } = useAuth();
if (isLoggedIn) {
return <Navigate to={`/${userRole}/dashboard`} replace />;
}
return children;
};
return (
<Routes>
{/* Public Path */}
<Route path="/" element={<PublicRoute />}>
<Route path="login" element={<LoginPage />} />
<Route path="sign-up" element={<SignupPage />} />
</Route>
{/* Protected routes */}
<Route path={`/${userRole}`} element={<ProtectedRoute />}>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="profile" element={<ProfilePage />} />
</Route>
{/* Buyer's routes */}
<Route path={`/${userRole}`} element={<PrivateRoute
allowedRoles={["buyer"]} />}>
<Route path="cart" element={<CartPage />} />
<Route path="wishlist" element={<WishlistPage />} />
</Route>
{/* Seller's routes */}
<Route path={`/${userRole}`} element={<ProtectedRoute
allowedRole={["seller"]} />}>
<Route path="post-product" element={<PostProduct />} />
<Route path="all-products" element={<AllProductsPage/>} />
</Route>
{/* Error handling routes */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
)
}
Whenever a user attempts to navigate to any public routes without logging out, the Public route component gets the current pathname and redirects the user to the path if the user is logged in. If the user is not logged in, the user can easily navigate to the unauthenticated pages.
We've defined three types of route components to handle different access levels:
PublicRoute
: This component is used for pages that should be accessible to all users, such as the home page, login page, and signup page. If a logged-in user tries to access these routes, they'll be redirected to their dashboard.
ProtectedRoute
: This component is used for pages that require authentication but are accessible to all authenticated users, regardless of their role. The dashboard and profile pages are examples of protected routes.
PrivateRoute
: This component is used for role-specific pages. It checks if the user is authenticated and also verifies if the user's role is included in the allowedRoles
array. This allows for flexible role-based access control.
Using these components, we can easily control access to different parts of our application based on authentication status and user roles.
Real-World Implications of Route Types
Understanding the practical applications and implications of different route types is crucial for building secure and user-friendly React applications. Let's explore each type in more detail:
Public routes are commonly used in landing pages, about pages, pricing information, and login and registration pages. Without proper PublicRoute implementation, logged-in users might face confusing situations. For instance, if a user bookmarks the login page and accesses it later while already logged in, they could see the login form again instead of their dashboard. This creates a poor user experience and might lead to unnecessary logout/login cycles.
Protected routes are used for user dashboards, account settings pages, and basic features available to authenticated users. Failing to implement protected routes could lead to security vulnerabilities. Unauthenticated users might gain access to sensitive information or functionality. For example, if a user's session expires while browsing their account settings, they should be immediately redirected to the login page to authenticate. This ensures the security of their personal information.
Private routes are used for role-based pages like admin panels for admins, seller dashboards (in e-commerce applications), and premium features available to paying users. Without private routes, your application might expose functionality or information to users who shouldn't have access. This not only creates security risks but also leads to a confusing user experience. For instance, in an e-commerce platform, a regular buyer shouldn't see options to add or edit product listings, which should be restricted to seller accounts.
By implementing these route types correctly, you ensure that users are guided to the appropriate parts of your application based on their authentication status and role, sensitive information and functionality are protected from unauthorized access, and the user interface remains clean and relevant to their permissions and needs.
Conclusion
In the article, you’ve learned how to implement Private, Protected, and Public routes and also their differences. These explicit route components provide a clear structure for implementing different levels of access control in your React application. They make it easy to manage public, protected, and role-specific routes, improving the security and organization of your application.
Also, you’ve touched a bit of authorization and authentication. For a better understanding of these concepts see my article on the topic: https://drprime.hashnode.dev/authentication-vs-authorization.