Skip to main content

React Router - Routing and Navigation

Question 1: How does React Router work? (BrowserRouter, Routes, Route)​

Answer:

React Router is the standard routing library for React applications, enabling client-side navigation without full page reloads. The core architecture consists of three main components: BrowserRouter (the router provider using HTML5 History API), Routes (the container for route definitions), and Route (individual route mappings). BrowserRouter wraps your entire application and provides routing context to all child components. Routes acts as a switch that renders the first matching route, replacing the old Switch component from v5. Route components define the mapping between URL paths and React components to render.

The library uses declarative routing where you define routes as JSX components rather than configuration objects. When a user navigates, React Router matches the current URL against all Route paths, renders the matching component, and updates the browser history. It leverages React context internally to share routing state (current location, navigation methods) across the component tree without prop drilling.

Key hooks enable programmatic navigation and route parameter access: useNavigate for imperative navigation, useParams for accessing URL parameters, useSearchParams for query string manipulation, and useLocation for current location details. The Outlet component enables nested routing by rendering child routes within parent layouts. This architecture supports complex routing patterns including nested routes, protected routes, lazy loading, and data fetching integration while maintaining React's component-based philosophy.

React Router v6 introduced significant improvements over v5: automatic route ranking (no need for exact prop), element-based route configuration (element prop instead of component), relative routing, and built-in support for data loading with loaders and actions in v6.4+.


πŸ” Deep Dive: Router Internals and Navigation Mechanisms​

Router Provider Architecture:

React Router uses React context to provide routing state throughout the application. BrowserRouter creates a router instance using the History API and provides it via context:

// Simplified internal structure
function BrowserRouter({ children }) {
// Create history instance using window.history
const historyRef = useRef();
if (!historyRef.current) {
historyRef.current = createBrowserHistory();
}

const [state, setState] = useState({
action: historyRef.current.action,
location: historyRef.current.location,
});

useLayoutEffect(() => {
// Listen for location changes
return historyRef.current.listen(setState);
}, []);

return (
<Router
navigator={historyRef.current}
location={state.location}
navigationType={state.action}
>
{children}
</Router>
);
}

Route Matching Algorithm:

React Router v6 uses automatic route ranking to match routes. The algorithm scores routes based on specificity:

// Route ranking factors (highest to lowest priority):
// 1. Static segments: /users/profile
// 2. Dynamic segments: /users/:id
// 3. Splat routes: /users/*
// 4. Index routes

// Example route scoring:
const routes = [
{ path: '/users/:id/edit', score: 9 }, // static + dynamic + static
{ path: '/users/:id', score: 6 }, // static + dynamic
{ path: '/users/new', score: 10 }, // static + static (exact match wins)
{ path: '/users/*', score: 3 }, // static + splat
];

// Internal matching function (simplified)
function matchRoutes(routes, location) {
// Rank routes by specificity
const rankedRoutes = routes
.map(route => ({
route,
score: computeScore(route.path),
match: matchPath(route.path, location.pathname)
}))
.filter(branch => branch.match)
.sort((a, b) => b.score - a.score);

return rankedRoutes[0]?.match || null;
}

Navigation Mechanisms:

React Router provides multiple navigation methods, each using the History API differently:

// 1. Declarative navigation with Link
function Link({ to, ...props }) {
const navigate = useNavigate();

function handleClick(event) {
if (!event.defaultPrevented &&
event.button === 0 &&
!props.target) {
event.preventDefault();
navigate(to);
}
}

return <a href={to} onClick={handleClick} {...props} />;
}

// 2. Programmatic navigation with useNavigate
const navigate = useNavigate();

// Push navigation (adds history entry)
navigate('/users/123'); // history.pushState()

// Replace navigation (replaces current entry)
navigate('/login', { replace: true }); // history.replaceState()

// Navigate with state
navigate('/dashboard', {
state: { from: location.pathname }
});

// Navigate relative to current route
navigate('../'); // Go up one level
navigate('edit'); // Go to sibling route

// Go back/forward
navigate(-1); // history.back()
navigate(1); // history.forward()

Location State Management:

React Router tracks location changes and notifies subscribed components:

// Internal location state (simplified)
const LocationContext = React.createContext({
location: { pathname: '/', search: '', hash: '', state: null },
navigationType: 'POP'
});

// useLocation implementation
function useLocation() {
const context = useContext(LocationContext);
if (!context) {
throw new Error('useLocation must be used within a Router');
}
return context.location;
}

// Listening to location changes
useEffect(() => {
const unlisten = history.listen(({ location, action }) => {
// Update router state
setState({ location, action });

// Scroll restoration
if (action === 'POP') {
// Browser back/forward - restore scroll
} else {
// New navigation - scroll to top
window.scrollTo(0, 0);
}
});

return unlisten;
}, [history]);

Nested Routes with Outlet:

Outlet is a placeholder that renders the next level of nested routes:

// Parent component with Outlet
function Layout() {
return (
<div>
<nav>Navigation</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}

// Internal Outlet implementation (simplified)
function Outlet({ context }) {
const outlet = useOutlet(context);
return outlet;
}

function useOutlet(context) {
const matches = useMatches(); // Get all matched routes
const childMatch = matches[matches.length - 1]; // Get deepest match

if (!childMatch) return null;

return (
<RouteContext.Provider value={context || childMatch.context}>
{childMatch.route.element}
</RouteContext.Provider>
);
}

Route Params Parsing:

Dynamic segments are extracted and provided via useParams:

// Route definition
<Route path="/users/:userId/posts/:postId" element={<Post />} />

// Internal params extraction (simplified)
function matchPath(pattern, pathname) {
const paramNames = [];
const regexpSource = pattern
.replace(/:(\w+)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)'; // Match any chars except /
});

const regexp = new RegExp(`^${regexpSource}$`);
const match = pathname.match(regexp);

if (!match) return null;

const params = {};
paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});

return { params, pathname: match[0] };
}

// useParams hook
function useParams() {
const matches = useMatches();
const currentMatch = matches[matches.length - 1];
return currentMatch?.params || {};
}

// Usage
function Post() {
const { userId, postId } = useParams();
// userId and postId are automatically decoded
}

This internal architecture enables React Router's declarative API while efficiently managing navigation state and route matching across complex application structures.


πŸ› Real-World Scenario: Route Mismatch and Navigation Bugs​

Scenario: E-commerce application with inconsistent navigation behavior. Users report: (1) Back button sometimes goes to wrong page, (2) Product details page shows "404" despite valid URLs, (3) Search filters lost after navigation, (4) Deep links fail to load nested routes.

Metrics:

  • Route mismatch errors: ~8% of navigations
  • Back button failures: ~12% of sessions
  • Lost filter state: ~25% of search navigations
  • Deep link failures: ~15% of shared URLs

Investigation:

// ❌ PROBLEMATIC CODE: Multiple routing issues

// Issue 1: Route order causes mismatches
function App() {
return (
<Routes>
<Route path="/products/*" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetails />} />
{/* Splat route matches first, :id never reached! */}
</Routes>
);
}

// Issue 2: Navigation without preserving state
function SearchFilters({ filters }) {
const navigate = useNavigate();

const applyFilters = (newFilters) => {
// Lost filters state on navigation!
navigate(`/products?category=${newFilters.category}`);
};

return <FilterForm onSubmit={applyFilters} />;
}

// Issue 3: Nested routes missing Outlet
function ProductLayout() {
return (
<div>
<ProductNav />
{/* Missing <Outlet /> - nested routes don't render! */}
</div>
);
}

// Issue 4: Relative navigation confusion
function ProductDetails() {
const navigate = useNavigate();

const goToReviews = () => {
// From /products/123, navigates to /reviews
// instead of /products/123/reviews
navigate('reviews'); // Wrong!
};
}

// Issue 5: Search params overwritten
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();

const setSortOrder = (order) => {
// Overwrites all existing params!
setSearchParams({ sort: order });
// Lost category, price filters, etc.
};
}

Debugging Steps:

// Step 1: Add route debugging middleware
function RouteDebugger() {
const location = useLocation();
const matches = useMatches();

useEffect(() => {
console.log('Navigation Event:', {
pathname: location.pathname,
search: location.search,
state: location.state,
matchedRoutes: matches.map(m => m.pathname)
});
}, [location]);

return null;
}

// Step 2: Inspect route matching
function RouteMatcher() {
const location = useLocation();

useEffect(() => {
// Log all potential matches
const allRoutes = [
'/products/*',
'/products/:id',
'/products/:id/reviews'
];

allRoutes.forEach(path => {
const match = matchPath(path, location.pathname);
console.log(`Route ${path}:`, match ? 'MATCH' : 'NO MATCH');
});
}, [location.pathname]);
}

// Step 3: Track navigation history
const navigationHistory = [];

function NavigationTracker() {
const location = useLocation();
const navigationType = useNavigationType();

useEffect(() => {
navigationHistory.push({
pathname: location.pathname,
type: navigationType,
timestamp: Date.now()
});

// Detect back button issues
if (navigationType === 'POP') {
const previous = navigationHistory[navigationHistory.length - 2];
console.log('Back navigation from:', previous?.pathname);
}
}, [location]);
}

Solution:

// βœ… FIXED: Proper route organization and state management

// Fix 1: Correct route order (specific before generic)
function App() {
return (
<Routes>
{/* Specific routes first */}
<Route path="/products/:id/reviews" element={<Reviews />} />
<Route path="/products/:id" element={<ProductDetails />} />
{/* Generic routes last */}
<Route path="/products" element={<ProductList />} />
<Route path="/products/*" element={<NotFound />} />
</Routes>
);
}

// Fix 2: Preserve search params during navigation
function SearchFilters() {
const [searchParams, setSearchParams] = useSearchParams();

const applyFilters = (newFilters) => {
// Preserve existing params
const current = Object.fromEntries(searchParams);
setSearchParams({
...current,
...newFilters
});
};

// Alternative: Use navigate with search string
const navigate = useNavigate();
const applyFiltersAlt = (newFilters) => {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
params.set(key, value);
});
navigate(`/products?${params.toString()}`);
};

return <FilterForm onSubmit={applyFilters} />;
}

// Fix 3: Add Outlet for nested routes
function ProductLayout() {
return (
<div>
<ProductNav />
<main>
<Outlet /> {/* Renders nested routes */}
</main>
</div>
);
}

// Fix 4: Use relative routing correctly
function ProductDetails() {
const navigate = useNavigate();

const goToReviews = () => {
// Relative to current route
navigate('reviews', { relative: 'path' });
// Or explicit absolute path
navigate(`/products/${productId}/reviews`);
};
}

// Fix 5: Navigation state preservation
function useNavigationState() {
const location = useLocation();
const navigate = useNavigate();

// Save scroll position before navigation
const saveScrollPosition = () => {
sessionStorage.setItem(
`scroll-${location.key}`,
JSON.stringify({
x: window.scrollX,
y: window.scrollY
})
);
};

// Restore scroll position after navigation
useEffect(() => {
const saved = sessionStorage.getItem(`scroll-${location.key}`);
if (saved) {
const { x, y } = JSON.parse(saved);
window.scrollTo(x, y);
}
}, [location.key]);

return { saveScrollPosition };
}

// Fix 6: Handle deep links with nested routes
function AppRouter() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="products" element={<ProductLayout />}>
<Route index element={<ProductList />} />
<Route path=":id" element={<ProductDetails />}>
<Route index element={<Overview />} />
<Route path="reviews" element={<Reviews />} />
<Route path="specs" element={<Specifications />} />
</Route>
</Route>
</Route>
</Routes>
);
}

Results After Fix:

  • Route mismatch errors: 8% β†’ 0.3%
  • Back button reliability: 88% β†’ 99.5%
  • Filter state preservation: 75% β†’ 99%
  • Deep link success rate: 85% β†’ 99.8%

Key Takeaways:

  1. Route order matters even with v6's automatic ranking
  2. Always preserve search params when updating them
  3. Nested routes require Outlet components
  4. Use relative navigation carefully with explicit relative prop
  5. Test deep links and back button behavior thoroughly

<details> <summary><strong>βš–οΈ Trade-offs: React Router vs Alternatives and Pattern Comparisons</strong></summary>

React Router vs TanStack Router:

AspectReact RouterTanStack RouterWhen to Choose
Type SafetyLimited (path strings)Full TypeScript inferenceTanStack for large TypeScript apps
Data LoadingLoaders (v6.4+)Built-in with suspenseTanStack for complex data needs
Bundle Size~15kb minified~20kb minifiedReact Router for size-sensitive apps
Learning CurveGentle (familiar API)Steeper (new concepts)React Router for quick onboarding
Search ParamsManual parsingType-safe schemasTanStack for complex filters
Code SplittingManual with lazy()Built-in route-basedTanStack for automatic optimization
// React Router: Manual type safety
function UserProfile() {
const { userId } = useParams(); // string | undefined
const id = parseInt(userId!, 10); // Manual parsing/validation
}

// TanStack Router: Type-safe params
const userRoute = new Route({
path: '/users/$userId',
parseParams: (params) => ({
userId: z.number().parse(Number(params.userId))
}),
});

function UserProfile() {
const { userId } = userRoute.useParams(); // number (type-safe!)
}

React Router vs Next.js App Router:

FeatureReact RouterNext.js App RouterTrade-off
RenderingClient-side onlyServer + clientNext.js for SEO, React Router for SPAs
File SystemJSX configurationFile-based routingNext.js for convention, React Router for flexibility
Data FetchingClient loadersServer componentsNext.js for server data, React Router for client control
DeploymentStatic hostingNode server requiredReact Router for simple deployment
BundleFull bundle upfrontAutomatic splittingNext.js for better initial load
// React Router: Explicit configuration
<Routes>
<Route path="/blog/:slug" element={<BlogPost />} loader={loadPost} />
</Routes>

// Next.js: File-based (app/blog/[slug]/page.tsx)
export default async function BlogPost({ params }) {
const post = await loadPost(params.slug); // Server-side
return <article>{post.content}</article>;
}

Declarative vs Programmatic Routing:

// βœ… Declarative (Preferred for most cases)
function Navigation() {
return (
<nav>
<Link to="/dashboard">Dashboard</Link>
<Link to="/profile">Profile</Link>
</nav>
);
}

// Pros: Accessible (native <a> tags), SEO-friendly, right-click works
// Cons: Less control over navigation flow

// βœ… Programmatic (Use for conditional logic)
function LoginForm() {
const navigate = useNavigate();
const location = useLocation();

const handleSubmit = async (credentials) => {
await login(credentials);
const from = location.state?.from || '/dashboard';
navigate(from, { replace: true });
};
}

// Pros: Full control, conditional navigation, replace history
// Cons: No SEO benefits, requires JavaScript

Route-Based vs Component-Based Code Splitting:

// βœ… Route-Based Splitting (Recommended)
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));

<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
} />
<Route path="/profile" element={
<Suspense fallback={<Loading />}>
<Profile />
</Suspense>
} />
</Routes>

// Pros: Automatic chunking, smaller initial bundle
// Cons: Loading states needed, network delay on navigation

// ❌ No Splitting (Avoid for large apps)
import Dashboard from './Dashboard';
import Profile from './Profile';

<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>

// Pros: Instant navigation, simpler code
// Cons: Large initial bundle, slower first load

Nested Routes vs Flat Routes:

// βœ… Nested Routes (Better for complex layouts)
<Routes>
<Route path="/" element={<Layout />}>
<Route path="products" element={<ProductLayout />}>
<Route index element={<ProductList />} />
<Route path=":id" element={<ProductDetails />} />
</Route>
</Route>
</Routes>

// Pros: Shared layouts, hierarchical structure, cleaner URLs
// Cons: More complex setup, Outlet placement critical

// βœ… Flat Routes (Better for simple apps)
<Routes>
<Route path="/" element={<Layout><Home /></Layout>} />
<Route path="/products" element={<Layout><ProductList /></Layout>} />
<Route path="/products/:id" element={<Layout><ProductDetails /></Layout>} />
</Routes>

// Pros: Simple, explicit, easier to understand
// Cons: Layout duplication, harder to maintain

Search Params vs URL Path Parameters:

// βœ… Path Params (Use for resource identity)
<Route path="/users/:userId" />
// URL: /users/123
const { userId } = useParams();

// Best for: User profiles, blog posts, product details
// Pros: SEO-friendly, shareable, hierarchical
// Cons: Not optional, harder to add/remove

// βœ… Search Params (Use for filters/options)
<Route path="/products" />
// URL: /products?category=electronics&sort=price
const [params] = useSearchParams();

// Best for: Filters, sorting, pagination, temporary state
// Pros: Optional, easily added/removed, multiple values
// Cons: Not hierarchical, can get messy

Decision Matrix:

Choose React Router when:

  • Building client-side SPAs
  • Need full routing control
  • Simple deployment (static hosting)
  • Familiar with React patterns

Choose TanStack Router when:

  • TypeScript-first development
  • Complex search param validation
  • Want built-in code splitting
  • Need type-safe navigation

Choose Next.js App Router when:

  • SEO is critical
  • Server-side data fetching needed
  • Want file-based routing
  • Building full-stack apps

πŸ’¬ Explain to Junior: Understanding React Router​

Simple Analogy:

Think of React Router like a GPS navigation system for your React app. Just like a GPS:

  1. BrowserRouter is the GPS device itself - it knows where you are and tracks your movement
  2. Routes is your route planner - it lists all possible destinations
  3. Route is a single destination with directions - "when you're at '/home', show the Home component"
  4. Link is like tapping a destination on the map - it changes your location
  5. useNavigate is like telling the GPS "take me to X" - programmatic directions

When you click a link, the GPS (Router) updates your location without reloading the entire car (app)!

Basic Implementation Walkthrough:

// Step 1: Wrap app with BrowserRouter (the GPS device)
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<Navigation />
<Routes>
{/* Define all destinations */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserProfile />} />
</Routes>
</BrowserRouter>
);
}

// Step 2: Create navigation (clickable destinations)
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User 123</Link>
</nav>
);
}

// Step 3: Access route information in components
function UserProfile() {
// Get the :id from the URL
const { id } = useParams();
return <h1>User Profile #{id}</h1>;
}

Common Interview Questions & Answers:

Q: What's the difference between Link and regular <a> tags?

A: "Link prevents full page reloads. Regular <a> tags cause the browser to reload the entire page, losing all React state. Link uses the History API to update the URL and tell React Router to render the new component, keeping your app state intact. Under the hood, Link still renders an <a> tag for accessibility, but intercepts the click event."

// ❌ Regular <a> - Full page reload
<a href="/about">About</a>

// βœ… Link - Client-side navigation
<Link to="/about">About</Link>

Q: How do you navigate programmatically (not with links)?

A: "Use the useNavigate hook. This is useful after form submissions, authentication, or conditional redirects. Call navigate with the path you want to go to."

function LoginForm() {
const navigate = useNavigate();

const handleLogin = async () => {
await loginUser();
navigate('/dashboard'); // Go to dashboard after login
};

return <button onClick={handleLogin}>Login</button>;
}

Q: How do you get parameters from the URL?

A: "Use useParams for path parameters (like /users/:id) and useSearchParams for query strings (like ?search=react). useParams returns an object with parameter names as keys."

// URL: /users/123?tab=posts
function UserProfile() {
const { id } = useParams(); // "123"
const [searchParams] = useSearchParams();
const tab = searchParams.get('tab'); // "posts"

return <div>User {id}, Tab: {tab}</div>;
}

Q: What is Outlet and when do you use it?

A: "Outlet is a placeholder for child routes. When you have nested routes, the parent component uses <Outlet /> to say 'render the child route here'. It's like a slot where the matching child component appears."

// Parent layout component
function Dashboard() {
return (
<div>
<nav>Dashboard Nav</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}

// Route configuration
<Route path="/dashboard" element={<Dashboard />}>
<Route path="overview" element={<Overview />} />
<Route path="settings" element={<Settings />} />
</Route>

// When user goes to /dashboard/overview:
// Dashboard renders with <Overview /> in the <Outlet />

Q: How do you create a 404 page?

A: "Use a Route with path='*' at the end of your Routes. The asterisk matches any path that doesn't match previous routes. Put it last so it only matches when nothing else does."

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* Catch-all */}
</Routes>

Step-by-Step Mental Model:

  1. Setup Phase: BrowserRouter wraps your app and starts listening to URL changes
  2. Navigation Event: User clicks a Link or you call navigate()
  3. URL Update: Browser URL changes (without reload) using History API
  4. Route Matching: React Router compares current URL against all Route paths
  5. Component Render: The matching Route's element component renders
  6. Cleanup: Previous component unmounts, new component mounts

Visual Flow:

User clicks <Link to="/users/123">
↓
BrowserRouter detects navigation
↓
URL changes to /users/123
↓
Routes compares all paths:
"/" β†’ no match
"/about" β†’ no match
"/users/:id" β†’ MATCH! (id = "123")
↓
<UserProfile /> renders with useParams() returning { id: "123" }

Common Beginner Mistakes:

// ❌ Mistake 1: Forgetting BrowserRouter
<Routes> {/* Won't work without BrowserRouter! */}
<Route path="/" element={<Home />} />
</Routes>

// βœ… Fix: Always wrap in BrowserRouter
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>

// ❌ Mistake 2: Using component instead of element
<Route path="/" component={Home} /> {/* Old v5 syntax */}

// βœ… Fix: Use element prop in v6
<Route path="/" element={<Home />} />

// ❌ Mistake 3: Nested Routes without Outlet
function Layout() {
return <div><nav>Nav</nav></div>; // Child routes won't show!
}

// βœ… Fix: Add Outlet
function Layout() {
return (
<div>
<nav>Nav</nav>
<Outlet /> {/* Children render here */}
</div>
);
}

Interview Answer Template:

"React Router enables client-side navigation in React apps without page reloads. The core components are BrowserRouter (wraps the app), Routes (container for route definitions), and Route (maps paths to components). When users navigate, React Router matches the URL against Route paths and renders the corresponding component. Key hooks include useNavigate for programmatic navigation, useParams for URL parameters, and useSearchParams for query strings. This enables smooth single-page app experiences while maintaining proper browser history and shareable URLs."


Question 2: How to implement protected routes and authentication?​

Answer:

Protected routes (also called private routes or guarded routes) restrict access to certain pages based on authentication or authorization status. The core pattern involves creating a wrapper component that checks authentication state before rendering the requested route, redirecting unauthenticated users to a login page while preserving the intended destination. This is implemented using React Router's navigation hooks and conditional rendering.

The typical implementation uses a ProtectedRoute component that wraps authenticated routes, checking if the user is logged in before rendering children. If authentication fails, it redirects to the login page using the navigate function, storing the original location in navigation state so users can be redirected back after successful authentication. This pattern supports various authorization levels: basic authentication (logged in or not), role-based access control (admin, user, guest), and permission-based access (specific capabilities).

Modern React Router v6 simplifies this pattern compared to v5. Instead of using a custom Route component, you create a wrapper component that uses standard Route elements and the Navigate component for redirects. The authentication state is typically managed via React context (AuthContext) that provides login status, user data, and authentication methods throughout the app.

Key implementation considerations include: handling async authentication checks (token validation, session verification), preserving intended navigation location, managing loading states during authentication verification, and implementing logout with proper cleanup. Advanced patterns support role-based routing, permission checks, and seamless redirects that maintain user flow while ensuring security.


πŸ” Deep Dive: Authentication Flow and Protected Route Internals​

Authentication Context Architecture:

A robust auth system requires centralized state management for user information and authentication status:

// Complete AuthContext implementation
const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

// Initialize auth state from token/session
useEffect(() => {
const initAuth = async () => {
try {
const token = localStorage.getItem('authToken');
if (token) {
// Validate token with backend
const userData = await validateToken(token);
setUser(userData);
}
} catch (error) {
console.error('Auth initialization failed:', error);
localStorage.removeItem('authToken');
} finally {
setLoading(false);
}
};

initAuth();
}, []);

const login = async (credentials) => {
try {
const { user, token } = await authenticateUser(credentials);
localStorage.setItem('authToken', token);
setUser(user);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
};

const logout = async () => {
try {
await logoutUser(); // Invalidate token on server
localStorage.removeItem('authToken');
setUser(null);
} catch (error) {
console.error('Logout failed:', error);
}
};

const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user,
};

return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}

Protected Route Implementation Patterns:

// Pattern 1: Basic Protected Route (simple auth check)
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();

if (loading) {
return <LoadingSpinner />;
}

if (!isAuthenticated) {
// Redirect to login, save intended destination
return <Navigate to="/login" state={{ from: location }} replace />;
}

return children;
}

// Usage
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

// Pattern 2: Role-Based Protected Route
function RoleProtectedRoute({ children, allowedRoles }) {
const { user, loading } = useAuth();
const location = useLocation();

if (loading) {
return <LoadingSpinner />;
}

if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}

if (!allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}

return children;
}

// Usage
<Route path="/admin" element={
<RoleProtectedRoute allowedRoles={['admin', 'moderator']}>
<AdminPanel />
</RoleProtectedRoute>
} />

// Pattern 3: Permission-Based Protected Route
function PermissionProtectedRoute({ children, requiredPermissions }) {
const { user, loading } = useAuth();
const location = useLocation();

if (loading) {
return <LoadingSpinner />;
}

if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}

const hasAllPermissions = requiredPermissions.every(
permission => user.permissions?.includes(permission)
);

if (!hasAllPermissions) {
return <Navigate to="/forbidden" replace />;
}

return children;
}

// Usage
<Route path="/users/delete" element={
<PermissionProtectedRoute requiredPermissions={['users.delete']}>
<DeleteUser />
</PermissionProtectedRoute>
} />

Login Flow with Redirect:

function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [error, setError] = useState('');

// Get intended destination from navigation state
const from = location.state?.from?.pathname || '/dashboard';

const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const credentials = {
email: formData.get('email'),
password: formData.get('password'),
};

const result = await login(credentials);

if (result.success) {
// Redirect to intended destination
navigate(from, { replace: true });
} else {
setError(result.error);
}
};

return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}

Advanced: Async Route Guards:

Some routes need async checks before rendering (e.g., checking permissions from API):

function AsyncProtectedRoute({ children, validate }) {
const [authorized, setAuthorized] = useState(null);
const { user } = useAuth();
const location = useLocation();

useEffect(() => {
let cancelled = false;

const checkAuthorization = async () => {
try {
// Custom async validation (API call, etc.)
const isAuthorized = await validate(user);
if (!cancelled) {
setAuthorized(isAuthorized);
}
} catch (error) {
if (!cancelled) {
setAuthorized(false);
}
}
};

checkAuthorization();

return () => {
cancelled = true;
};
}, [user, validate]);

if (authorized === null) {
return <LoadingSpinner />;
}

if (!authorized) {
return <Navigate to="/forbidden" state={{ from: location }} replace />;
}

return children;
}

// Usage: Check if user can access specific resource
<Route path="/projects/:id/settings" element={
<AsyncProtectedRoute
validate={async (user) => {
const project = await fetchProject(params.id);
return project.ownerId === user.id;
}}
>
<ProjectSettings />
</AsyncProtectedRoute>
} />

Token Refresh and Session Management:

// Axios interceptor for automatic token refresh
function setupAuthInterceptor() {
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// Token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
// Refresh token
const refreshToken = localStorage.getItem('refreshToken');
const { accessToken } = await refreshAuthToken(refreshToken);

// Update stored token
localStorage.setItem('authToken', accessToken);

// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh failed, logout user
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);
}

// Session timeout detection
function useSessionTimeout(timeoutMinutes = 30) {
const { logout } = useAuth();
const timeoutRef = useRef(null);

const resetTimeout = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
logout();
alert('Session expired due to inactivity');
}, timeoutMinutes * 60 * 1000);
}, [logout, timeoutMinutes]);

useEffect(() => {
// Track user activity
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];

events.forEach(event => {
document.addEventListener(event, resetTimeout);
});

resetTimeout(); // Initialize timeout

return () => {
events.forEach(event => {
document.removeEventListener(event, resetTimeout);
});
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [resetTimeout]);
}

Complete Route Configuration:

function AppRoutes() {
return (
<Routes>
{/* Public routes */}
<Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />

{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

{/* Role-based routes */}
<Route path="/admin" element={
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
} />

{/* Nested protected routes */}
<Route path="/app" element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}>
<Route index element={<Overview />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>

{/* Error routes */}
<Route path="/unauthorized" element={<Unauthorized />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}

This architecture provides secure, scalable authentication with proper session management, token refresh, and flexible authorization patterns.


πŸ› Real-World Scenario: Authentication Redirect Loop and State Management​

Scenario: SaaS application experiencing authentication issues. Users report: (1) Infinite redirect loops between /login and /dashboard, (2) Lost authentication state on page refresh, (3) Unauthorized access to protected pages despite logout, (4) Original navigation destination lost after login.

Metrics:

  • Redirect loop incidents: ~5% of login attempts
  • Session loss on refresh: ~30% of authenticated sessions
  • Unauthorized access after logout: ~8% of sessions
  • Failed redirect to intended page: ~40% of login flows

Investigation:

// ❌ PROBLEMATIC CODE: Multiple auth issues

// Issue 1: Redirect loop - ProtectedRoute redirects to Login,
// Login redirects to Dashboard, Dashboard is protected...
function ProtectedRoute({ children }) {
const { user } = useAuth();

if (!user) {
// Missing replace prop causes history pollution
return <Navigate to="/login" />;
}

return children;
}

function LoginPage() {
const { user } = useAuth();
const navigate = useNavigate();

// Redirects immediately if user exists
if (user) {
navigate('/dashboard'); // But dashboard redirects back!
}

return <LoginForm />;
}

// Issue 2: No loading state during auth check
function ProtectedRoute({ children }) {
const { user } = useAuth(); // user is null during loading!

// Redirects before auth check completes
if (!user) {
return <Navigate to="/login" replace />;
}

return children;
}

// Issue 3: Lost location state
function LoginPage() {
const navigate = useNavigate();

const handleLogin = async (credentials) => {
await login(credentials);
// Always goes to dashboard, ignoring where user tried to go
navigate('/dashboard');
};
}

// Issue 4: Stale auth state after logout
function LogoutButton() {
const { logout } = useAuth();
const navigate = useNavigate();

const handleLogout = () => {
logout(); // Async, doesn't wait
navigate('/login'); // Navigates before logout completes!
};
}

// Issue 5: Token not attached to requests
function AuthProvider({ children }) {
const [user, setUser] = useState(null);

const login = async (credentials) => {
const { user, token } = await authenticate(credentials);
setUser(user);
// Token stored but never used in requests!
localStorage.setItem('token', token);
};

return <AuthContext.Provider value={{ user, login }}>{children}</AuthContext.Provider>;
}

Debugging Steps:

// Step 1: Add navigation tracking
function NavigationLogger() {
const location = useLocation();
const navigationType = useNavigationType();

useEffect(() => {
console.log('Navigation:', {
pathname: location.pathname,
state: location.state,
type: navigationType,
timestamp: new Date().toISOString()
});
}, [location, navigationType]);

return null;
}

// Step 2: Monitor auth state changes
function AuthDebugger() {
const auth = useAuth();

useEffect(() => {
console.log('Auth State:', {
user: auth.user,
loading: auth.loading,
isAuthenticated: auth.isAuthenticated,
timestamp: new Date().toISOString()
});
}, [auth]);

return null;
}

// Step 3: Track token lifecycle
const originalSetItem = localStorage.setItem;
const originalRemoveItem = localStorage.removeItem;

localStorage.setItem = function(key, value) {
console.log('localStorage.setItem:', key, value);
originalSetItem.apply(this, arguments);
};

localStorage.removeItem = function(key) {
console.log('localStorage.removeItem:', key);
originalRemoveItem.apply(this, arguments);
};

Solution:

// βœ… FIXED: Proper auth implementation with safeguards

// Fix 1: ProtectedRoute with loading state and replace
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const location = useLocation();

// Wait for auth check to complete
if (loading) {
return (
<div className="loading-container">
<LoadingSpinner />
</div>
);
}

// Redirect if not authenticated
if (!user) {
return (
<Navigate
to="/login"
state={{ from: location }} // Save intended destination
replace // Don't pollute history
/>
);
}

return children;
}

// Fix 2: Login page with proper redirect handling
function LoginPage() {
const { user, login, loading } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [error, setError] = useState('');

// Get intended destination from state
const from = location.state?.from?.pathname || '/dashboard';

// Redirect if already logged in (but only after loading)
useEffect(() => {
if (!loading && user) {
navigate(from, { replace: true });
}
}, [user, loading, navigate, from]);

const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);

try {
await login({
email: formData.get('email'),
password: formData.get('password'),
});
// Navigation happens in useEffect above
} catch (err) {
setError(err.message);
}
};

// Don't show login form if already authenticated
if (loading) return <LoadingSpinner />;
if (user) return null; // useEffect will redirect

return (
<form onSubmit={handleSubmit}>
{error && <Alert variant="error">{error}</Alert>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}

// Fix 3: AuthProvider with proper initialization
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

// Initialize auth from stored token
useEffect(() => {
const initAuth = async () => {
const token = localStorage.getItem('authToken');

if (token) {
try {
// Validate token and get user data
const userData = await validateToken(token);
setUser(userData);
setupAxiosAuth(token);
} catch (error) {
console.error('Token validation failed:', error);
localStorage.removeItem('authToken');
}
}

setLoading(false);
};

initAuth();
}, []);

const login = async (credentials) => {
try {
const { user, token } = await authenticateUser(credentials);

// Store token
localStorage.setItem('authToken', token);

// Setup axios to include token in requests
setupAxiosAuth(token);

// Update state
setUser(user);

return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
};

const logout = async () => {
try {
// Call logout endpoint
await logoutUser();
} catch (error) {
console.error('Logout API call failed:', error);
} finally {
// Always clear local state, even if API fails
localStorage.removeItem('authToken');
setUser(null);

// Remove token from axios
delete axios.defaults.headers.common['Authorization'];
}
};

const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user,
};

return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}

// Fix 4: Setup axios to include auth token
function setupAxiosAuth(token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;

// Intercept 401 responses
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Token invalid, clear auth and redirect
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}

// Fix 5: Proper logout with navigation
function LogoutButton() {
const { logout } = useAuth();
const navigate = useNavigate();

const handleLogout = async () => {
await logout(); // Wait for logout to complete
navigate('/login', { replace: true });
};

return <button onClick={handleLogout}>Logout</button>;
}

// Fix 6: Handle edge case - concurrent auth checks
function useAuth() {
const context = useContext(AuthContext);

if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider');
}

return context;
}

// Fix 7: Complete route configuration
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Landing />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />

{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

<Route path="/profile" element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
} />

<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}

Results After Fix:

  • Redirect loops: 5% β†’ 0%
  • Session persistence: 70% β†’ 99.5%
  • Unauthorized access: 8% β†’ 0%
  • Successful redirects: 60% β†’ 99%

Key Takeaways:

  1. Always include loading state in ProtectedRoute
  2. Use replace prop to prevent history pollution
  3. Preserve intended destination in location state
  4. Wait for async operations before navigation
  5. Setup axios interceptors for token management
  6. Clear auth state completely on logout

</details>

<details> <summary><strong>βš–οΈ Trade-offs: Protection Patterns and Auth Strategies</strong></summary>

Component-Based vs Loader-Based Protection:

AspectComponent WrapperRoute Loader (v6.4+)When to Choose
Protection PointRender timeBefore navigationLoader for data-dependent auth
Loading UXComponent shows spinnerNavigation pendingLoader for cleaner UX
Code LocationWraps Route elementIn route configWrapper for simplicity
Data FetchingAfter auth checkIntegrated with authLoader for combined fetch+auth
ReusabilityWrap multiple routesPer-route configWrapper for bulk protection
// βœ… Component-Based (Traditional)
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

// Pros: Simple, reusable, works with any React Router version
// Cons: Flash of redirect, component mounts then unmounts

// βœ… Loader-Based (React Router v6.4+)
<Route
path="/dashboard"
element={<Dashboard />}
loader={async ({ request }) => {
const user = await checkAuth();
if (!user) {
throw redirect('/login');
}
return { user };
}}
/>

// Pros: Check before render, cleaner UX, integrated with data loading
// Cons: Requires v6.4+, different pattern, more setup

Client-Side vs Server-Side Auth:

// βœ… Client-Side (React Router)
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();

if (loading) return <Loading />;
if (!user) return <Navigate to="/login" />;
return children;
}

// Pros: Fast navigation, works offline, simple deployment
// Cons: Vulnerable to tampering, must validate on API, SEO issues

// βœ… Server-Side (Next.js)
export async function getServerSideProps(context) {
const session = await getSession(context);

if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}

return {
props: { user: session.user },
};
}

// Pros: Secure, SEO-friendly, no flash of unauth content
// Cons: Server required, slower navigation, more complex deployment

Token Storage Strategies:

LocationSecurityPersistenceXSS VulnerableCSRF VulnerableBest For
localStorageLowYes (permanent)YesNoLong sessions, SPAs
sessionStorageLowNo (tab close)YesNoTemporary sessions
Cookie (httpOnly)HighYesNoYesHigh security apps
Memory onlyMediumNo (refresh)NoNoHighly sensitive data
// βœ… localStorage (Common for SPAs)
const login = async (credentials) => {
const { token } = await authenticate(credentials);
localStorage.setItem('authToken', token);
};

// Pros: Persists across tabs, simple API
// Cons: Accessible to JavaScript (XSS risk)

// βœ… httpOnly Cookie (Most Secure)
const login = async (credentials) => {
// Server sets httpOnly cookie
await authenticate(credentials);
// Token not accessible to JavaScript
};

// Pros: Protected from XSS, automatic sending
// Cons: CSRF risk, requires CSRF tokens, server setup

// βœ… Memory + Refresh Token Pattern
const [accessToken, setAccessToken] = useState(null);

const login = async (credentials) => {
const { accessToken, refreshToken } = await authenticate(credentials);
setAccessToken(accessToken); // Memory only
localStorage.setItem('refreshToken', refreshToken); // Long-lived
};

// Pros: Short-lived access token in memory, can refresh
// Cons: Complex implementation, lost on refresh

Role vs Permission-Based Authorization:

// βœ… Role-Based (Simpler)
function AdminRoute({ children }) {
const { user } = useAuth();

if (user.role !== 'admin') {
return <Navigate to="/forbidden" />;
}

return children;
}

// Pros: Simple to implement, easy to understand
// Cons: Rigid, hard to customize per user

// βœ… Permission-Based (Flexible)
function PermissionRoute({ children, requires }) {
const { user } = useAuth();

const hasPermission = requires.every(
perm => user.permissions.includes(perm)
);

if (!hasPermission) {
return <Navigate to="/forbidden" />;
}

return children;
}

// Usage
<PermissionRoute requires={['users.delete', 'users.edit']}>
<UserManagement />
</PermissionRoute>

// Pros: Granular control, flexible, scalable
// Cons: More complex, requires permission system

Redirect vs Conditional Rendering:

// βœ… Redirect (Better UX)
function ProtectedRoute({ children }) {
const { user } = useAuth();

if (!user) {
return <Navigate to="/login" replace />;
}

return children;
}

// Pros: Clear separation, URL reflects state
// Cons: Extra navigation, history management

// βœ… Conditional Render (Inline)
function Dashboard() {
const { user } = useAuth();

if (!user) {
return <LoginPrompt />;
}

return <DashboardContent />;
}

// Pros: No navigation, faster, keeps URL
// Cons: URL doesn't reflect auth state, confusing

Optimistic vs Pessimistic Auth Checks:

// βœ… Optimistic (Assume valid until proven otherwise)
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();

// Show content immediately if user exists
if (user) return children;

// Check still loading
if (loading) return <Loading />;

// Failed auth check
return <Navigate to="/login" />;
}

// Pros: Faster perceived performance, less loading
// Cons: Flash of unauthorized content possible

// βœ… Pessimistic (Wait for confirmation)
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();

// Always wait for loading to complete
if (loading) return <Loading />;

if (!user) {
return <Navigate to="/login" />;
}

return children;
}

// Pros: Secure, no flash of wrong content
// Cons: Always shows loading, slower

Decision Matrix:

Choose Component Protection when:

  • Using React Router v5 or v6 (any version)
  • Need simple, reusable pattern
  • Don't need data loading integration

Choose Loader Protection when:

  • Using React Router v6.4+
  • Want to integrate auth with data fetching
  • Need cleaner UX without redirect flash

Choose localStorage when:

  • Building typical SPA
  • XSS protection is in place
  • Need simple implementation

Choose httpOnly Cookies when:

  • Security is critical
  • Have server-side capability
  • Can implement CSRF protection

Choose Permission-Based when:

  • Need granular control
  • Users have varying capabilities
  • Roles alone are too rigid

πŸ’¬ Explain to Junior: Implementing Protected Routes​

Simple Analogy:

Think of protected routes like a bouncer at a VIP club:

  1. ProtectedRoute component = Bouncer checking IDs at the door
  2. AuthContext = The bouncer's list of allowed guests
  3. Login page = Where you get your VIP wristband
  4. Navigate component = Bouncer redirecting you to get a wristband
  5. Location state = Remembering which VIP room you originally tried to enter

When you try to enter the VIP room (protected route), the bouncer (ProtectedRoute) checks if you have a wristband (authentication). No wristband? They send you to the wristband desk (login page) and remember which room you wanted (location state). After getting your wristband, they let you into the room you originally wanted!

Step-by-Step Implementation:

// Step 1: Create AuthContext to manage user state
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

function AuthProvider({ children }) {
const [user, setUser] = useState(null);

const login = async (credentials) => {
// Fake login - replace with real API call
const fakeUser = { id: 1, name: 'John', role: 'user' };
setUser(fakeUser);
localStorage.setItem('user', JSON.stringify(fakeUser));
};

const logout = () => {
setUser(null);
localStorage.removeItem('user');
};

return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}

// Hook to use auth anywhere in app
function useAuth() {
return useContext(AuthContext);
}

// Step 2: Create ProtectedRoute component
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();

// If no user, redirect to login
if (!user) {
return (
<Navigate
to="/login"
state={{ from: location }} // Remember where they tried to go
replace // Don't add to history
/>
);
}

// User is logged in, show the page
return children;
}

// Step 3: Create Login page
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();

// Get where user tried to go before being redirected
const from = location.state?.from?.pathname || '/dashboard';

const handleSubmit = async (e) => {
e.preventDefault();
await login({ email: '...', password: '...' });
navigate(from); // Send them where they originally wanted to go
};

return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
);
}

// Step 4: Use ProtectedRoute in your routes
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Anyone can see these */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<LoginPage />} />

{/* Must be logged in to see these */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

<Route path="/profile" element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}

Common Interview Questions & Answers:

Q: How do you protect a route in React Router?

A: "Create a ProtectedRoute component that checks if the user is authenticated. If they are, render the children (the protected page). If not, redirect them to the login page using the Navigate component. Wrap any protected routes with this component."

function ProtectedRoute({ children }) {
const { user } = useAuth();

if (!user) {
return <Navigate to="/login" replace />;
}

return children;
}

// Usage
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />

Q: How do you redirect users back to their intended page after login?

A: "Use location state to save where they tried to go. When ProtectedRoute redirects to login, pass the current location in state. After successful login, navigate to that saved location instead of always going to the same page."

// In ProtectedRoute: Save intended destination
<Navigate to="/login" state={{ from: location }} />

// In LoginPage: Redirect to intended destination
const from = location.state?.from?.pathname || '/dashboard';
navigate(from); // Goes where they originally tried to go

Q: How do you implement role-based routes (like admin-only pages)?

A: "Extend the ProtectedRoute pattern to check user roles. Create a RoleProtectedRoute component that checks both authentication and whether the user's role is in the list of allowed roles."

function RoleProtectedRoute({ children, allowedRoles }) {
const { user } = useAuth();

if (!user) {
return <Navigate to="/login" />;
}

if (!allowedRoles.includes(user.role)) {
return <Navigate to="/forbidden" />; // Not authorized
}

return children;
}

// Usage: Only admins can access
<Route path="/admin" element={
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
} />

Q: Where should you store the authentication token?

A: "For most SPAs, localStorage is common because it persists across page refreshes and is easy to use. For higher security, use httpOnly cookies (set by the server) which can't be accessed by JavaScript, protecting against XSS attacks. Avoid storing sensitive tokens in regular cookies or session storage unless you have a specific reason."

// localStorage approach (common)
const login = async (credentials) => {
const { user, token } = await loginAPI(credentials);
localStorage.setItem('authToken', token);
setUser(user);
};

// Check on app load
useEffect(() => {
const token = localStorage.getItem('authToken');
if (token) {
validateTokenAndSetUser(token);
}
}, []);

Q: How do you handle logout?

A: "Call a logout function that clears the user state and removes the token from storage, then navigate to the login page. Make sure logout is async and you wait for it to complete before navigating."

function LogoutButton() {
const { logout } = useAuth();
const navigate = useNavigate();

const handleLogout = async () => {
await logout(); // Clear user state and token
navigate('/login', { replace: true });
};

return <button onClick={handleLogout}>Logout</button>;
}

// In AuthContext
const logout = async () => {
localStorage.removeItem('authToken');
setUser(null);
// Optionally call API to invalidate token on server
};

Common Beginner Mistakes:

// ❌ Mistake 1: Not preserving intended destination
function ProtectedRoute({ children }) {
const { user } = useAuth();

if (!user) {
return <Navigate to="/login" />; // Lost where they tried to go!
}

return children;
}

// βœ… Fix: Save location in state
<Navigate to="/login" state={{ from: location }} replace />

// ❌ Mistake 2: No loading state
function ProtectedRoute({ children }) {
const { user } = useAuth();

// If auth is loading, user is null, redirects to login!
if (!user) return <Navigate to="/login" />;
return children;
}

// βœ… Fix: Wait for loading to complete
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;

// ❌ Mistake 3: Not using replace prop
<Navigate to="/login" /> // Adds to history

// βœ… Fix: Use replace to avoid back button issues
<Navigate to="/login" replace />

// ❌ Mistake 4: Checking auth on every render
function ProtectedRoute({ children }) {
const [user, setUser] = useState(null);

// Runs on EVERY render!
checkAuth().then(setUser);

if (!user) return <Navigate to="/login" />;
return children;
}

// βœ… Fix: Check in context/provider, not in route
// Auth check should happen once in AuthProvider

Interview Answer Template:

"To implement protected routes, I create a ProtectedRoute wrapper component that checks authentication state from an AuthContext. If the user is authenticated, it renders the children components. If not, it redirects to the login page using Navigate, storing the intended destination in location state. After successful login, I redirect users back to where they originally tried to go. For role-based access, I extend this pattern to check user roles against allowed roles for each route. The authentication state is managed in a context provider that handles login, logout, and token storage in localStorage."

</details>



Source: routing-01a-react-router.md