Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Building Reliable Protected Routes with React Router v7
Matvey Romanov
Matvey Romanov

Posted on

     

Building Reliable Protected Routes with React Router v7

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 un­authenticated 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;};
Enter fullscreen modeExit fullscreen mode

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/>);};
Enter fullscreen modeExit fullscreen mode

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;
Enter fullscreen modeExit fullscreen mode

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>);}
Enter fullscreen modeExit fullscreen mode

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/>;};
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
pherman profile image
Paige Herman
  • Joined

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!

CollapseExpand
 
ra1nbow1 profile image
Matvey Romanov
Hello, world! I'm a professional self taught full-stack developer from Moscow. I truly love web-development and all that it concerns. Making websites is awesome. Follow me if you need some help
  • Location
    Moscow, Russia
  • Work
    Fullstack developer, Team lead, DEV Mod
  • Joined

Thank you for such a nice feedback!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Hello, world! I'm a professional self taught full-stack developer from Moscow. I truly love web-development and all that it concerns. Making websites is awesome. Follow me if you need some help
  • Location
    Moscow, Russia
  • Work
    Fullstack developer, Team lead, DEV Mod
  • Joined

More fromMatvey Romanov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp