
Why You Need This
Imagine your site is a hip nightclub. The main doors are open to all, but there’s a VIP area guarded by a bouncer: you need a secret pass (token) to get in. That bouncer on the front end is exactly what Protected Routes are for—keeping unauthenticated users out of private pages.
React Router v6 finally gave us the tools (like<Outlet />
,<Navigate />
and nested routes) to build this without hacky workarounds.
Setting Up My AuthContext
First, I created an AuthContext with React’s Context API to hold:
•isAuthenticated
: whether the user is logged in
•isLoading
: whether we’re still checking their token
•userRole
: optional, for role-based guards
•login
/logout
functions
This is like having a shared pizza fund: any component can peek in and see if there’s enough dough (credentials) to grab a slice (access)!
// AuthContext.tsximportReact,{createContext,useContext,useState,useEffect}from'react';interfaceAuthContextType{isAuthenticated:boolean;isLoading:boolean;userRole?:'admin'|'user';login:()=>Promise<void>;logout:()=>void;}constAuthContext=createContext<AuthContextType|undefined>(undefined);exportconstAuthProvider:React.FC<{children:React.ReactNode}>=({children})=>{const[isAuthenticated,setIsAuthenticated]=useState(false);const[isLoading,setIsLoading]=useState(true);const[userRole,setUserRole]=useState<'admin'|'user'>();useEffect(()=>{// On mount, check token validity with serverasyncfunctioncheckAuth(){try{// pretend fetch to validate tokenconstres=awaitfetch('/api/auth/validate');constdata=awaitres.json();setIsAuthenticated(data.ok);setUserRole(data.role);}catch{setIsAuthenticated(false);}finally{setIsLoading(false);}}checkAuth();},[]);constlogin=async()=>{// call login API, then:setIsAuthenticated(true);setUserRole('user');};constlogout=()=>{// clear token, etc.setIsAuthenticated(false);setUserRole(undefined);};return(<AuthContext.Providervalue={{isAuthenticated,isLoading,userRole,login,logout}}>{children}</AuthContext.Provider>);};// Custom hook for easy accessexportconstuseAuth=()=>{constctx=useContext(AuthContext);if(!ctx)thrownewError('useAuth must be inside AuthProvider');returnctx;};
My “Digital Bouncer”: the PrivateRoute Component
This component checks auth status, shows a loader while we wait, then either renders the protected content via<Outlet />
or redirects to/login
, carrying along where we came from.
// PrivateRoute.tsximport{Navigate,Outlet,useLocation}from'react-router-dom';import{useAuth}from'./AuthContext';exportconstPrivateRoute:React.FC=()=>{const{isAuthenticated,isLoading}=useAuth();constlocation=useLocation();if(isLoading){// Still verifying token—show a spinner or messagereturn<div>Loadingauthenticationstatus…</div>;}// If logged in, render child routes; otherwise redirect to /loginreturnisAuthenticated?(<Outlet/>):(<Navigateto="/login"replacestate={{from:location}}// remember original page/>);};
Wrapping Routes with the Bouncer
In your main router file (e.g.App.tsx
), group all private pages under one<Route element={<PrivateRoute />}>
. It’s like fencing off the VIP area in one go:
// App.tsximport{BrowserRouter,Routes,Route}from'react-router-dom';import{AuthProvider}from'./AuthContext';import{PrivateRoute}from'./PrivateRoute';importHomefrom'./Home';importLoginfrom'./Login';importDashboardfrom'./Dashboard';importProfilefrom'./Profile';functionApp(){return(<AuthProvider><BrowserRouter><Routes><Routepath="/"element={<Home/>}/>{/* Protected “VIP” routes */}<Routeelement={<PrivateRoute/>}><Routepath="/dashboard"element={<Dashboard/>}/><Routepath="/profile"element={<Profile/>}/></Route><Routepath="/login"element={<Login/>}/></Routes></BrowserRouter></AuthProvider>);}exportdefaultApp;
Speeding Things Up with Lazy Loading
To keep our main bundle slim, I wrapped my private pages in React.lazy and , so they load only when someone actually goes looking for them—like serving dishes only when ordered:
// LazyRoutes.tsximportReact,{Suspense,lazy}from'react';import{Routes,Route}from'react-router-dom';import{PrivateRoute}from'./PrivateRoute';constDashboard=lazy(()=>import('./Dashboard'));constProfile=lazy(()=>import('./Profile'));constLogin=lazy(()=>import('./Login'));exportdefaultfunctionLazyRoutes(){return(<Suspensefallback={<div>Loadingmodule…</div>}><Routes><Routepath="/login"element={<Login/>}/><Routeelement={<PrivateRoute/>}><Routepath="/dashboard"element={<Dashboard/>}/><Routepath="/profile"element={<Profile/>}/></Route></Routes></Suspense>);}
Bonus: Role-Based Gates and Memory
If you need role checks, add anallowedRoles
prop toPrivateRoute
:
// Extended PrivateRoute with rolesinterfacePrivateRouteProps{allowedRoles?:Array<'admin'|'user'>;}exportconstPrivateRoute:React.FC<PrivateRouteProps>=({allowedRoles})=>{const{isAuthenticated,isLoading,userRole}=useAuth();constlocation=useLocation();if(isLoading)return<div>Loading…</div>;if(!isAuthenticated){return<Navigateto="/login"replacestate={{from:location}}/>;}// If roles are provided, check themif(allowedRoles&&!allowedRoles.includes(userRole!)){// Could show a “403 Forbidden” page insteadreturn<Navigateto="/unauthorized"replace/>;}return<Outlet/>;};
And thanks tostate.from
in<Navigate />
, after a successful login you can send the user right back where they came from—like bookmarking their spot in the club.
What I Took Away from This
•Centralized & DRY: One context + one route guard—no copy-paste checks.
•Clear analogies: Bouncer, VIP, pizza fund—keeps concepts memorable.
•Performance: Lazy loading private modules keeps initial load quick.
•Flexibility: Easy to layer in roles, custom redirects, and more.
Give your feedback and follow myGithub
Top comments(2)

Great post! The nightclub metaphor had me picturing my React app with a velvet rope and some very picky bouncers in bowties, which honestly makes authentication way more fun to think about. I appreciate how you broke down the setup for protected routes and explained the benefits of using a single context and PrivateRoute component—less copy-pasting is always a win in my book (I’m lazy, just like your loaded components). The bonus about role-based access is super useful, since even my imaginary club needs an “Only For DJs” lounge. Thanks for making the concepts clear and leaving my mental pizza fund untouched. Looking forward to checking out more of your code on Github!

- LocationMoscow, Russia
- WorkFullstack developer, Team lead, DEV Mod
- Joined
Thank you for such a nice feedback!
For further actions, you may consider blocking this person and/orreporting abuse