React Router enables client-side navigation by intercepting URL changes via the History API, no page reloads.
Key components: BrowserRouter, Routes, Route, and Outlet for nested layouts.
Dynamic segments like :id are captured via useParams() — always returns strings, parse explicitly.
Protected routes use a wrapper component with to redirect before rendering child routes.
Production pitfall: Missing param in useEffect dependency array causes stale data after navigation.
✦ Definition~90s read
What is React Router?
React Router is the de facto standard client-side routing library for React applications, used by over 60% of React projects according to npm downloads. It solves a fundamental problem: mapping URL paths to component trees without full page reloads, enabling single-page applications (SPAs) to feel like multi-page sites.
★
Imagine a hotel with dozens of rooms.
Without it, you'd be manually managing window.location and popstate events, which quickly becomes unmanageable as your app grows. React Router gives you declarative, composable routing — you define routes as JSX or config objects, and it handles history, URL parsing, and component rendering.
It's not for server-rendered apps (use Next.js or Remix for that) or for simple static sites where a hash-based router like react-router-hash-link suffices.
Under the hood, React Router wraps the browser's History API (pushState, replaceState, popstate) and provides a context-based router that matches the current URL against a tree of route definitions. When a route matches, it renders the corresponding component and passes URL parameters via hooks like useParams.
The library uses a path-matching algorithm (based on path-to-regexp) that supports dynamic segments (:id), wildcards (*), and nested layouts via the <Outlet> component. This nesting is key: each <Route> with children renders an <Outlet> placeholder, allowing parent layouts (nav bars, sidebars) to persist while child routes swap in and out — no prop drilling or manual state management required.
React Router v6 introduced useRoutes, a hook that lets you define routes as a plain JavaScript array of objects instead of JSX. This is particularly useful for dynamic route generation, code splitting, or when routes come from an API. The config mirrors the JSX structure: { path: '/users/:id', element: <UserProfile />, children: [...] }.
Under the hood, useRoutes compiles this config into the same internal route tree as JSX, so performance and behavior are identical. The trade-off is less visual hierarchy in your code — you lose the immediate readability of nested <Route> tags, but gain flexibility for programmatic routing scenarios like role-based access control or A/B testing.
Plain-English First
Imagine a hotel with dozens of rooms. The front desk is the main entrance — but depending on which room number you ask for, the receptionist sends you to a completely different place. React Router is that receptionist. Your app has one entrance (index.html), and React Router decides which 'room' (component) to show based on the URL the user visits. No page reload, no server trip — just instant redirection handled entirely in the browser.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
You've built a beautiful React app, but your users are hitting 404s on page reload. Or worse, your analytics show zero page views because every route returns the same component. These aren't edge cases—they're the result of treating React Router like a magic black box. After debugging production routing failures at scale, I've learned that most developers misuse React Router in ways that silently break their apps. The default <BrowserRouter> works fine in development, but deploy it behind a CDN or a reverse proxy, and suddenly your routes vanish. The fix isn't more configuration—it's understanding how the router actually resolves URLs. In this article, we'll strip away the abstractions and look at React Router's core mechanics: how it intercepts navigation, matches routes, and renders components. You'll learn why your SPA breaks on refresh, how to handle dynamic segments without leaking state, and when to ditch client-side routing entirely. No fluff, just the patterns that survive production.
What React Router Actually Does
React Router is a client-side routing library that maps URL paths to React components, enabling single-page applications (SPAs) to render different views without a full page reload. Its core mechanic is declarative route configuration: you define a tree of elements, and the library matches the current URL against them to decide which component tree to mount. This matching is path-first, not component-first — the URL drives the UI, not the other way around.
Under the hood, React Router uses the History API (pushState, replaceState, popstate) to synchronize the browser's address bar with your component hierarchy. When a user clicks a or your code calls navigate(), the library intercepts the event, updates the URL via the History API, and then re-renders the matched route tree. It does not fetch new HTML from the server — all transitions happen in memory, which is why it's fast (sub-millisecond route matching) but also why server-side rendering requires extra care.
Use React Router when you need deep-linkable, bookmarkable URLs in a React SPA. It's the standard choice for most React applications because it integrates tightly with React's component model and provides hooks like useParams, useNavigate, and useLocation. In production, it's critical to understand that route matching is greedy by default — the first matching wins — and that nested layouts require careful outlet placement. Misconfiguring redirects or guards is the leading cause of infinite loops, especially when authentication logic lives inside route components.
⚠ Route Matching Is Not Regex
React Router uses path-to-regexp for matching, not full regex. A path like "/users/:id" matches "/users/42" but not "/users/42/edit" unless you add a wildcard or exact prop.
📊 Production Insight
A fintech dashboard app deployed a protected route with a <Navigate> inside the auth guard component. When the token was expired, the guard redirected to /login, but /login itself had a guard that checked for a token and redirected back to /dashboard. The result: a browser tab spinning at 100% CPU, 500+ redirects per second, and a crash within 3 seconds. Rule: never place redirect logic inside a component that is rendered by the same router that handles the target route — use a layout route or a before-enter hook instead.
🎯 Key Takeaway
Route matching is path-first and greedy — order your <Route> elements from most specific to least specific.
Redirects inside route components are a direct path to infinite loops — always handle auth in a layout route or a wrapper component that does not render <Navigate>.
Use the <Outlet> pattern for nested layouts; forgetting it causes blank pages or broken hierarchies.
thecodeforge.io
React Router
How React Router Actually Works Under the Hood
Most tutorials skip straight to <Route> syntax without explaining the machinery. Let's fix that.
React Router is built on the History API — a browser-native interface that lets JavaScript manipulate the URL bar without triggering a page load. When you call navigate('/dashboard'), React Router pushes a new entry onto the browser's history stack, reads the new URL, matches it against your route definitions, and re-renders the matching components. The server never sees this navigation.
This is why wrapping your app in <BrowserRouter> is non-negotiable. It creates a context that holds the current location and exposes it to every nested component. Without it, hooks like useNavigate and useParams have no context to read from and will throw.
React Router v6 (the current version) made a significant architectural shift: <Switch> was replaced with <Routes>, and route matching became exclusive by default — meaning the first match wins and only that component renders. You no longer need the exact prop. Routes also became composable, so nested routes live inside parent components rather than all being declared in one giant config file.
AppRouter.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// AppRouter.jsx — the root routing setup for a small e-commerce appimportReactfrom'react';
import { BrowserRouter, Routes, Route } from'react-router-dom';
importMainLayoutfrom'./layouts/MainLayout';
importHomePagefrom'./pages/HomePage';
importProductListPagefrom'./pages/ProductListPage';
importProductDetailPagefrom'./pages/ProductDetailPage';
importNotFoundPagefrom'./pages/NotFoundPage';
exportdefaultfunctionAppRouter() {
return (
// BrowserRouter creates the History API context — everything inside// can read the current URL and react to navigation events
<BrowserRouter>
<Routes>
{/*
The path="/" route wraps shared UI (nav, footer) via MainLayout.
Child routes render inside MainLayout's <Outlet /> placeholder.
*/}
<Route path="/" element={<MainLayout />}>
{/* index means: render this when path is exactly "/" */}
<Route index element={<HomePage />} />
{/* Static nested route — maps to /products */}
<Route path="products" element={<ProductListPage />} />
{/*
Dynamic segment: the colon prefix (:productId) means
"capture whatever is here as a named parameter".
/products/42 → productId = "42"
/products/apple-watch → productId = "apple-watch"
*/}
<Route path="products/:productId" element={<ProductDetailPage />} />
{/* Catch-all: path="*" matches anything that didn't match above */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
Output
// No console output — this is a declarative component tree.
In v5, <Switch> rendered the first matching <Route>. In v6, <Routes> does the same thing by default — every route match is exclusive. The upgrade also means you no longer litter your route config with exact props, which was one of the most common sources of routing bugs in v5 apps.
📊 Production Insight
The History API is not available in all environments — HashRouter uses the URL hash to avoid server round-trip.
In production, if you see a flash of unstyled content during route transitions, it's likely because your router is not wrapping the entire app tree.
Rule: always wrap the top-level component with <BrowserRouter> — missing it throws silent errors that look like 'useNavigate() may be used only in the context of a <Router> component.'
🎯 Key Takeaway
React Router uses the History API to change the URL without reloading the page.
Route matching in v6 is exclusive by default — first match wins, no exact prop needed.
Forgot the router? You'll get cryptic errors. Wrap your app once, at the root.
Dynamic Routes, URL Params and the useParams Hook
Static routes handle predictable paths like /about or /contact. But real apps deal with data-driven URLs — product pages, user profiles, order receipts — where the path contains an ID that determines what gets fetched. That's where dynamic segments come in.
When you define path="products/:productId", React Router captures the :productId segment and makes it available via the useParams() hook inside the rendered component. The returned value is always a string, so if your API expects a number, you must parse it explicitly — a silent bug that bites constantly.
You can have multiple dynamic segments in one path: path="orders/:orderId/items/:itemId" is perfectly valid. Each segment becomes a key in the object useParams() returns.
One pattern worth knowing: when a URL param changes (e.g., navigating from /products/1 to /products/2), React doesn't unmount and remount the component — it just re-renders it with new params. This means any useEffect that fetches data based on a param must list that param as a dependency, otherwise you'll display stale data from the previous route.
ProductDetailPage.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ProductDetailPage.jsx — reads a URL param and fetches matching product dataimportReact, { useEffect, useState } from'react';
import { useParams, useNavigate, Link } from'react-router-dom';
// Simulated async API call — replace with your real fetch/axios callasyncfunctionfetchProductById(id) {
const mockProducts = {
'1': { id: 1, name: 'Wireless Headphones', price: 89.99, stock: 14 },
'2': { id: 2, name: 'Mechanical Keyboard', price: 129.99, stock: 0 },
};
// Simulate network latencyawaitnewPromise(resolve => setTimeout(resolve, 400));
return mockProducts[id] ?? null; // Return null if product doesn't exist
}
exportdefaultfunctionProductDetailPage() {
// useParams returns an object matching your route's dynamic segments// { productId: "1" } — always a string, even if the URL contains "1"const { productId } = useParams();
const navigate = useNavigate(); // For programmatic navigationconst [product, setProduct] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetchProductById(productId)
.then(data => {
if (!data) {
// Product doesn't exist — redirect to the 404 pagenavigate('/not-found', { replace: true });
return;
}
setProduct(data);
})
.finally(() => setIsLoading(false));
// CRITICAL: productId must be in the dependency array.// Without it, navigating from /products/1 to /products/2// would show product 1's data on the /products/2 URL.
}, [productId, navigate]);
if (isLoading) return <p>Loading product...</p>;
if (!product) return null; // navigate() is already firingreturn (
<div className="product-detail">
<Link to="/products">← Back to all products</Link>
<h1>{product.name}</h1>
<p>Price: ${product.price.toFixed(2)}</p>
<p>Status: {product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}</p>
<button
disabled={product.stock === 0}
onClick={() => navigate('/cart', { state: { productId: product.id } })}
>
Add to Cart
</button>
</div>
);
}
If your API expects GET /products/1 with a numeric ID, don't pass productId directly from useParams() — it's the string "1", not the number 1. Use parseInt(productId, 10) or Number(productId) before passing it to any function that needs a real number. TypeScript's React Router types will catch this at compile time, which is a great reason to add TS to your project.
📊 Production Insight
Missing param in useEffect dependency is the #1 cause of stale data in production.
I've seen teams debug for hours — user navigates from product 1 to product 2, but product 1 data persists because the effect never re-ran.
Rule: every value from useParams() that you use inside an effect must be in its dependency array. End of story.
🎯 Key Takeaway
useParams() always returns strings — parse them before API calls or comparisons.
If your component doesn't re-fetch when the URL changes, check the useEffect dependency array.
The component stays alive; only the params change. You must react to that change explicitly.
thecodeforge.io
React Router
Nested Layouts with Outlet — Shared UI Without Repetition
Almost every real app has persistent UI — a navigation bar, a sidebar, a footer — that should stay visible while the main content area changes. The naive approach is to import and render these components in every single page component. That works until you need to change the nav and have to update 20 files.
React Router's <Outlet> component solves this cleanly. A parent route renders a layout component that contains <Outlet /> as a placeholder. When a child route matches, React Router injects that child's component into the <Outlet /> position. The parent layout re-renders zero times — only the outlet's content swaps.
This pattern scales beautifully. You can nest layouts multiple levels deep — an app layout containing an authenticated layout containing a settings layout — each adding its own framing UI without knowing anything about its children.
One underused feature: you can pass context data down through <Outlet context={value} /> and read it in child routes via useOutletContext(). This is useful for passing things like the current authenticated user without threading props manually.
Use <Link> for any navigation where the visual state doesn't matter (e.g., a 'Back' button). Use <NavLink> for navigation menus where you want the current item styled differently — it automatically receives an isActive boolean in its className and style callbacks, saving you from writing a custom active-detection hook.
📊 Production Insight
A common mistake: forgetting to add <Outlet /> in the parent layout. The result? Child routes render nothing, and you spend an hour debugging.
Another trap: passing context via Outlet is great, but if you update the context value on every render, all children re-render unnecessarily. Memoize it.
Rule: <Outlet /> is not optional — if you want child routes to render, it must be present in the parent's JSX.
🎯 Key Takeaway
Use parent routes with <Outlet /> to share layouts without duplicating JSX.
Pass context via Outlet's context prop to avoid prop drilling.
Forget the Outlet? Child routes disappear. Double-check it every time you add a layout.
Nested Routing Visual Hierarchy: How Routes Map to Components
One of the trickiest concepts for devs moving from React Router v5 to v6 is understanding how nested routes compose into a component tree. In v5, nested routes were defined flat — the parent route's component had to manually render components for its children. In v6, the hierarchy is visual: your JSX tree directly mirrors the component tree that renders.
Consider this route config
/ → MainLayout (renders navbar, footer, and )
/ index → HomePage
/products → ProductListPage
/products/:productId → ProductDetailPage
/dashboard → ProtectedRoute (wraps after auth check)
/dashboard index → DashboardHome
/dashboard/settings → SettingsPage
When a user visits /dashboard/settings, the component tree is: ```` The URL changes, but the tree stays exactly the same shape. React only swaps the deepest matching component. This is why performance is excellent and transitions feel instant.
The diagram below illustrates this hierarchy — each route is a node, and nesting shows parent-child relationships.
AppRouter.jsx (nested example)JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Visual representation of the nested route structure// This code is not meant to be run; it shows the tree shape.
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<HomePage />} />
<Route path="products">
<Route index element={<ProductListPage />} />
<Route path=":productId" element={<ProductDetailPage />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route path="dashboard">
<Route index element={<DashboardHome />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
Each <Route> in v6 corresponds to a node in the component tree. The parent route's element (like MainLayout) is always rendered, and its <Outlet /> becomes the placeholder for the active child. This makes debugging easier: you can inspect the React DevTools component tree to see exactly which routes are active.
📊 Production Insight
In production, the most common issue with nested routes is forgetting to nest <Route> elements properly. If a child route doesn't render, check that it is indeed a child of the parent <Route> block, and that the parent's element includes <Outlet />. Also ensure that ProtectedRoute (or any wrapper) renders <Outlet /> and not the child route directly — this is a frequent source of empty pages.
🎯 Key Takeaway
Nested routes in v6 form a direct parent-child component hierarchy — the JSX tree mirrors the render tree, making the flow intuitive once you see the pattern.
Nested Route Hierarchy — Each node is a route path or layout wrapper
/ (MainLayout)
index (HomePage)
/products
index (ProductListPage)
/products/:productId (ProductDetailPage)
useRoutes Hook: Declare Routes as a Config Object
While JSX-based <Routes> and <Route> components work fine, some teams prefer defining routes as a plain JavaScript object — especially when routes need to be generated dynamically (e.g., from a CMS or API). React Router's useRoutes hook accepts an array of route configuration objects and returns the matching element tree.
This approach is particularly useful for
Server-side route configuration (share route definitions between client and server)
Dynamic route generation (add/remove routes based on user permissions)
Reducing JSX nesting in complex apps
Easier testing (route config is a plain array)
The config object mirrors the structure of <Route>: path, element, children, index, loader, action, errorElement, and so on.
One caveat:useRoutes cannot be used outside of a <BrowserRouter> or <Router> context, just like any other routing hook. Also, it returns null if no route matches — you'll need a fallback component or a catch-all route.
useRoutesExample.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// routes.config.js — shared route configuration objectconst routes = [
{
// Parent route with layout
path: '/',
element: <MainLayout />,
children: [
{ index: true, element: <HomePage /> },
{ path: 'products', element: <ProductListPage /> },
{ path: 'products/:productId', element: <ProductDetailPage /> },
{
// Protected routes — use a wrapper element
element: <ProtectedRoute />,
children: [
{
path: 'dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'settings', element: <SettingsPage /> },
],
},
],
},
{ path: '*', element: <NotFoundPage /> },
],
},
];
exportdefault routes;
// App.jsx — useRoutes hookimport { useRoutes } from'react-router-dom';
import routes from'./routes.config';
exportdefaultfunctionApp() {
const element = useRoutes(routes);
// If no route matches, element will be null
return element ?? <NotFoundPage />; // fallback
}
// Alternative: pass a fallback route inside the config// Add { path: '*', element: <NotFoundPage /> } at the end of children.
Output
// The output is identical to using JSX <Routes> — React Router handles the same matching logic.
Use JSX for small, static apps where readability counts. Use useRoutes when routes are generated dynamically (e.g., from user permissions) or when you need to serialize route config (e.g., for SSR). The two are interchangeable — you can even mix them within the same app (not recommended for clarity).
📊 Production Insight
One production issue with useRoutes is that it returns null when no route matches, which can cause a blank page if you don't handle it. Always provide a fallback (either in the config or as a conditional render). Also, if you update the route config dynamically (e.g., after fetching user roles), the component using useRoutes will re-render and the routes will update — but all mounted child components (like those using useParams) will unmount and remount. For smooth transitions, consider wrapping the output in <AnimatedOutlet> or using a key to preserve state.
🎯 Key Takeaway
useRoutes lets you define routes as a plain array of objects — useful for dynamic routing and easy config serialization. Always handle the null return case to prevent blank pages.
Data API: Loaders and Actions for Route-Level Data Fetching
React Router v6.4 introduced a new Data API that fundamentally changes how you handledata fetching and mutations. With createBrowserRouter, you can define loader and actionfunctions directly on a route definition. The loader runs before the route component renders, and the action handles form submissions (POST, PUT, DELETE). This eliminates the classic useEffect + useState pattern for data loading and gives you built-in error handling, pending states, and automatic revalidation.
Loader: An async function that returns data to the route component via useLoaderData(). It runs when the route is entered or when a parent loader changes. The component can access the data synchronously (no loading state management).
Action: An async function that handles form submissions. It receives the form data and can return a response (including redirects). After the action runs, all loaders on the current page automatically revalidate — no manual refetching needed.
Key benefits
No more useEffect for data fetching — loaders run before render, preventing flash of loading.
Built-in optimistic UI via useNavigation() and useFetcher().
Error handling: define an errorElement on any route to catch loader/action errors.
Parallel loading: sibling routes load data simultaneously.
This pattern is especially powerful for data-driven apps like dashboards or e-commerce sites where route transitions should fetch fresh data without complex state management.
🔥When to Use the Data API vs Classical React Router
If your app relies on client-side data fetching with useEffect and already works fine, you don't need to migrate. The Data API is best for new projects or sections with heavy data dependencies (dashboards, e-commerce). It pairs well with server-side rendering (Remix-style) but works entirely client-side too.
📊 Production Insight
One gotcha: loaders run on every navigation to that route, even if the data hasn't changed. For performance-sensitive routes, implement caching inside the loader (e.g., a simple Map or a library like react-query). Also, using useNavigation().state to show a spinner during transitions can cause a flash if the network is fast — consider debouncing or using a minimum delay. Finally, never use useLoaderData() outside of a route component that has a loader defined — it will throw an error.
🎯 Key Takeaway
Loaders and actions provide route-level data fetching and mutation with automatic revalidation, error boundaries, and zero boilerplate. They replace useEffect for data loading in modern React Router apps.
Protected Routes — Redirect Unauthenticated Users the Right Way
Every app with user accounts has pages that should only be accessible when logged in. A naive approach might be checking auth state inside each page component and calling navigate('/login') if the user isn't authenticated. That works, but it means scattering auth logic across every protected page — a maintenance nightmare.
The idiomatic React Router pattern is a ProtectedRoute wrapper component. It reads auth state in one place, and if the user isn't authenticated, it immediately renders <Navigate /> to redirect them before any protected UI ever renders. If they are authenticated, it renders <Outlet /> to let the matched child route through.
The key detail is storing the originally requested URL in the redirect. When a logged-out user hits /dashboard, you want to send them to /login?redirect=/dashboard so after they authenticate, you can send them exactly where they wanted to go — not just to a generic home page. This is the difference between a frustrating UX and a seamless one.
This pattern also composes naturally with role-based access: you can create a <RequiresAdminRole /> wrapper that checks the user's role, not just whether they're logged in.
ProtectedRoute.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// ProtectedRoute.jsx — guards any routes nested inside itimportReactfrom'react';
import { Navigate, Outlet, useLocation } from'react-router-dom';
// In a real app this hook reads from your auth context / Zustand store / ReduxfunctionuseAuthStatus() {
// Simulating: user is NOT logged in// Swap to `return { isAuthenticated: true, user: { role: 'admin' } }` to test the happy pathreturn { isAuthenticated: false, user: null };
}
/**
* ProtectedRoute wraps any routes that require authentication.
* Use it as a parent route in your router config — child routes
* render via <Outlet /> only when the user is authenticated.
*
* @param {string} requiredRole - Optional. If provided, also checks user.role.
*/
exportdefaultfunctionProtectedRoute({ requiredRole = null }) {
const { isAuthenticated, user } = useAuthStatus();
// useLocation gives us the current URL so we can redirect back after loginconst currentLocation = useLocation();
// ── STEP 1: Is the user logged in at all? ──if (!isAuthenticated) {
/*
<Navigate> is the declarative way to redirect during render.
Don't use useNavigate() inside the render path — it can cause
issues withReact's concurrent rendering.
`state` carries the originally-requested path so the login page
can redirect back after a successful login.
`replace` means the login redirect won't create a new history entry —
pressing Back won't loop the user through the login page again.
*/
return (
<Navigate
to="/login"
state={{ redirectAfterLogin: currentLocation.pathname }}
replace
/>
);
}
// ── STEP 2: Does this route require a specific role? ──if (requiredRole && user?.role !== requiredRole) {
// User is logged in but lacks the required permissionreturn <Navigate to="/unauthorized" replace />;
}
// ── STEP 3: All checks passed — render the child routes ──return <Outlet />;
}
// ─────────────────────────────────────────────────────────────────────────────// HOW TO USE THIS IN YOUR ROUTER CONFIG://// <Routes>// <Route path="/" element={<MainLayout />}>// <Route index element={<HomePage />} />// <Route path="products" element={<ProductListPage />} />//// {/* All routes inside here require login */}// <Route element={<ProtectedRoute />}>// <Route path="dashboard" element={<DashboardPage />} />// <Route path="orders" element={<OrdersPage />} />// </Route>//// {/* This route also requires the "admin" role */}// <Route element={<ProtectedRoute requiredRole="admin" />}>// <Route path="admin" element={<AdminPanel />} />// </Route>// </Route>// </Routes>// ─────────────────────────────────────────────────────────────────────────────// LoginPage.jsx — reads the redirect state and uses it after loginexportfunctionLoginPage() {
const navigate = useNavigate();
const location = useLocation();
// Pull out where the user was trying to go before being redirectedconst redirectTarget = location.state?.redirectAfterLogin ?? '/dashboard';
functionhandleSuccessfulLogin() {
// ... your real auth logic here ...// Once auth succeeds, send them where they originally wanted to gonavigate(redirectTarget, { replace: true });
}
return (
<div>
<h1>LogIn</h1>
<button onClick={handleSuccessfulLogin}>SimulateLogin</button>
</div>
);
}
⚠ Watch Out: Don't Use useNavigate() During Render
A common mistake is calling const navigate = useNavigate() and then immediately calling navigate('/login') at the top of a component body (outside useEffect). This fires during the render cycle and can cause React warnings about state updates during render. Always use the declarative <Navigate /> component for redirect-on-render scenarios, and keep useNavigate() for event handlers and effects only.
📊 Production Insight
I've seen a production incident where an auth guard used synchronous localStorage check and caused an infinite redirect loop — users were stuck between /dashboard and /login.
The fix was to introduce an async auth context that provides a loading state. The protected route shows a spinner while auth resolves, then redirects only after confirming unauthenticated.
Rule: never redirect during render based on synchronous state — use asynchronous context and <Navigate> inside a conditional render block.
🎯 Key Takeaway
Protected routes: wrap child routes with a component that conditionally renders <Navigate> or <Outlet>.
Store the original URL in location state to redirect back after login.
Avoid useNavigate() during render — use <Navigate /> instead.
Programmatic Navigation and Route Transitions
Sometimes you need to navigate in response to an event — form submission, button click, timeout — not just a link click. React Router's useNavigate() hook gives you a function you can call anywhere in your component (inside event handlers, effects, callbacks) to navigate programmatically.
useNavigate() returns a function: navigate(to, options). The to argument is a path or a number (-1 for back, 1 for forward). Options include replace (boolean) and state (any serializable data to pass to the target route). Pass state when you need to communicate transient data like 'order just placed' without polluting the URL.
One difference from : programmatic navigation doesn't automatically handle 'open in new tab'. If you need that, use a with target="_blank" or construct an anchor tag manually with window.open().
Route transitions can be enhanced with animations using libraries like framer-motion or react-transition-group. The key is wrapping the element that changes (often the ) in an animation component that listens to route changes via useLocation().
Also worth knowing: React Router v6.4+ introduced useBlocker() and usePrompt() to prevent navigation when there are unsaved changes — critical for forms.
CartPage.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// CartPage.jsx — demonstrates programmatic navigation, state passing, and blocking navigationimportReact, { useState } from'react';
import { useNavigate, useBlocker } from'react-router-dom';
exportdefaultfunctionCartPage() {
const navigate = useNavigate();
const [cartItems, setCartItems] = useState([/* ... */]);
const [isCheckoutPending, setIsCheckoutPending] = useState(false);
// Block navigation if checkout is pending (prevent accidental back navigation)const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isCheckoutPending && currentLocation.pathname !== nextLocation.pathname
);
functionhandleCheckout() {
setIsCheckoutPending(true);
// Simulate API callsetTimeout(() => {
// After success, navigate to confirmation page with order datanavigate('/order-confirmation', {
state: { orderId: 123, total: 49.99 },
replace: true // Don't keep /checkout in history
});
}, 2000);
}
functiongoBack() {
navigate(-1); // Go back to previous page
}
return (
<div>
<h2>YourCart</h2>
{/* ... */}
<button onClick={handleCheckout} disabled={isCheckoutPending}>
{isCheckoutPending ? 'Processing...' : 'Checkout'}
</button>
<button onClick={goBack}>Back</button>
{blocker.state === 'blocked' && (
<div>
<p>You have an ongoing checkout. Leave anyway?</p>
<button onClick={() => blocker.proceed()}>Leave</button>
<button onClick={() => blocker.reset()}>Stay</button>
</div>
)}
</div>
);
}
// On the receiving end (OrderConfirmationPage.jsx):// import { useLocation } from 'react-router-dom';// const location = useLocation();// const orderId = location.state?.orderId; // 123
Mental Model: Programmatic Navigation as Side Effect
Think of navigate() as a side-effect: it should only be called in response to an event or after async work, not during render.
If you find yourself calling navigate() at the top of a component (outside useEffect), refactor to use <Navigate /> or move it into an event handler.
Use replace: true for redirects that should not create new history entries (e.g., after login, to prevent the user from going back to the login page).
Pass state via options.state — it's ephemeral, lost on hard refresh. Never use it for critical data.
For 'open in new tab', use a normal <Link> or window.open() — programmatic navigation can't open new tabs.
📊 Production Insight
A common mistake: calling navigate() inside an effect without proper dependencies, leading to infinite redirects or stale navigation targets.
Another trap: passing large objects in state that cause performance issues because they are serialized on every navigation.
Rule: Keep state small and primitive. For complex data, use a global store and pass only an ID or key in route state.
🎯 Key Takeaway
useNavigate() returns a function for event-driven navigation — don't call it during render.
Use navigate(-1) for back buttons, navigate(path, { replace: true }) for redirects.
Route state is ephemeral; use it for transient UI data, not business-critical information.
Route Error Boundaries: Why 404s Shouldn't Crash Your App
You've seen it. A user navigates to a deleted product page, your loader throws, and React unmounts the entire tree. That's not just bad UX, it's a production incident waiting to happen. React Router 6.4+ ships with errorElement — a dedicated component that catches loader and render errors per route.
Think of it as a try/catch for your routing layer. When a loader throws or a component crashes during render, the router unwinds up the route tree until it finds the nearest errorElement. It does NOT unmount sibling routes. Your sidebar, header, or layout stays alive. Only the broken outlet gets replaced.
You don't need a global ErrorBoundary class. You don't need to wrap every page manually. Just slap an errorElement on the parent layout route and handle 404s, 500s, or auth failures in one place. The router passes the error via useRouteError(). Inspect the status, render a fallback, log it. Done.
ErrorBoundaryLayout.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — javascript tutorialimport { Outlet, useRouteError, isRouteErrorResponse } from'react-router-dom';
exportdefaultfunctionRootLayout() {
return (
<div className="app-shell">
<header>StaticNav</header>
<Outlet />
</div>
);
}
// Matches parent route, wraps ALL childrenexportfunctionLayoutErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <h1>This page left the building.</h1>;
}
// Log to Sentry/DataDog in productionreturn <h1>Something broke. We're on it.</h1>;
}
Don't put errorElement on leaf routes unless you want per-page error handling. Put one on the root layout. The router walks UP the tree — a layout error catches all children.
🎯 Key Takeaway
Always attach an errorElement to your root layout route. It catches loader, action, and render errors without killing the UI shell.
Resource Routes: File Downloads Without Client-Side Bloat
Your dashboard needs to export a CSV. Most devs reach for Fetch + Blob, then manually trigger a download. That works, but it's an extra round trip and you're shipping download logic to every browser. Resource routes in React Router let your route handler return raw data — not a component.
Declare a route with loader but no element. The loader returns a Response object with the right Content-Type and Content-Disposition headers. The browser handles the download natively. No component, no useEffect, no state management.
This is huge for server-rendered or Remix-style apps, but it works in client-side React Router too if you configure a custom data router. The route acts like an API endpoint inside your routing tree. You get the same auth protection, loaders, and params — but the output is a file, not JSX.
Use resource routes for any non-interactive endpoint — PDF invoices, image transforms, JSON feeds. One less API endpoint to maintain.
🎯 Key Takeaway
If a route returns a file, not a UI, drop the element and return a Response from the loader. The browser does the rest.
Route Module Lazy Loading: Shrink Your Initial Bundle by 60%
Your bundle is 2MB. Users on 3G are staring at a white screen. Lazy loading isn't a React Router feature — it's React.lazy() + Suspense. But the way you wire it into routes matters. The pattern: import routes as promises, wrap them in lazy(), and set a fallback loader.
React Router 6+ supports React.lazy natively. You don't need react-loadable or custom async wrappers. Each route file becomes a separate chunk in Webpack or Vite. The first paint includes only the login page code. The dashboard — all 40 components — gets loaded only when the user navigates there.
The gotcha: lazy-loaded routes lose the ability to use loader/action from the same file unless you export them separately. The fix? Export your loader function and import it alongside the lazy component. The router will call the loader before the chunk finishes loading. Waterfall avoided.
Do NOT lazy-load the root layout. It's always needed. Lazy-load only page-level routes. And never wrap the Suspense fallback inside the lazy component — it won't work.
🎯 Key Takeaway
Lazy-load every route except your login and shell layout. Pair lazy() with a Suspense fallback and export loaders separately to avoid waterfall delays.
Features: React Router 6+ Is Not the Router You Remember
React Router v6 ripped out the legacy API and replaced it with something that actually works like React. No more Switch, no more Redirect, no more manual history stack management. The core features now revolve around data-driven routing: loaders, actions, and useFetcher replace half the boilerplate you used to write in useEffect.
The router is declarative. You define routes as a tree, and the router figures out which component to render based on the URL — no imperative history.push calls unless you really want them. BrowserRouter is still the default, but createBrowserRouter with data APIs is the new standard for production apps. Features like lazy loading, error boundaries, and nested routes are baked in, not bolted on.
If you're coming from v5, unlearn everything. v6 is a different beast — leaner, faster, and built for concurrent React. Features are not optional sprinkles; they're the foundation.
Don't mix v5 components with v6 routers — Switch and useHistory are gone. Run the migration codemod or rip the bandage off.
🎯 Key Takeaway
Features are not optional; v6 is a complete rewrite. Use createBrowserRouter and data loaders for any app that survives a deploy.
useHistory Is Dead — Long Live useNavigate and the Data API
You might be searching for useHistory because some ancient blog told you it's how you navigate programmatically. Wrong. That hook was removed in React Router v6. The replacement is useNavigate — a function you call to push, replace, or go back. But here's the production truth: if you're calling navigate more than once per user action, you're probably doing something wrong.
Router v6's data API (loaders, actions, and redirect) eliminates the need for manual navigation in most cases. When a form action runs, you can redirect the user to another route directly from the loader — no client-side navigation code needed. useNavigate is for edge cases: modal flows, undo gestures, or when you need to push state along with the URL.
The old useHistory pattern of reading from a global history object is gone because it was bug-prone and broke concurrent rendering. useNavigate is stable, predictable, and pairs with useLocation for reading the current URL. Stop searching for useHistory — it's dead. Bury it.
Use redirect in a loader or action instead of navigate for route-level redirects — it's framework-optimized and avoids unnecessary renders.
🎯 Key Takeaway
useHistory is v5 baggage. Use useNavigate for programmatic navigation, but prefer data API redirects for production flows.
Route Index Files: Default Children That Eliminate Empty Parent Routes
Index routes solve a common pain point: parent routes with layouts that render nothing by default. When you nest routes like /dashboard/settings and /dashboard/profile, the parent /dashboard path shows an empty outlet unless you create a dedicated Home.js. An index route is the default child that renders when the parent path exactly matches. It lives at the parent's URL but renders inside its Outlet. This keeps your route tree clean — no extra path strings, no conditional rendering in the parent component. React Router picks the index route automatically when no other child path matches. Use index for landing pages, dashboards, or any parent layout that needs a default view, not for 404s.
Forgetting index on a parent route causes a blank page at the exact path — the Outlet waits for a matching child that never appears.
🎯 Key Takeaway
Every parent route with an Outlet should have an index route to avoid empty UI.
Route Path Resolution: Absolute vs. Relative Paths and the Trailing Slash Gotcha
Path resolution in nested routes is subtle but critical. Absolute paths start with / and ignore parent nesting — they mount at the root. Relative paths without / inherit their parent's path, building deeper URLs like breadcrumbs. A common mistake: /dashboard and dashboard behave differently. The trailing slash also matters — /dashboard matches exactly, while /dashboard/ matches the same URL but treats it as a directory. When you use relative paths, React Router resolves them against the parent's matched path, not its declared path. Use relative paths for most nested routes to keep your tree maintainable. Prefer absolute paths only for top-level sections like /login or /admin that don't need a parent wrapper.
Accidentally using an absolute path (starting with /) inside a nested route breaks your layout hierarchy — the parent's Outlet is skipped entirely.
🎯 Key Takeaway
Always start nested route paths without / unless you intentionally want to escape the parent layout.
Setting Up React Router Pages: Home, Blogs, Contact
A clean route structure starts with well-defined page components. React Router doesn’t enforce any file layout, but separating concerns between routing logic and UI components keeps your app maintainable. The standard approach is to create a pages/ folder with one file per route view. Home, Blogs, and Contact each export a default component returning JSX. This isolation means you can load data, handle errors, and test pages independently. After defining the components, you import them into your router configuration. Use a top-level BrowserRouter and nest <Route> elements inside a <Routes> block. Each route maps a path (e.g., /blogs) to its component via the element prop. The result: a declarative, readable map of your app’s URLs to the screens users see. No magic — just intentional page setup that scales with your app.
Home.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — javascript tutorialconstHome = () => {
return (
<main>
<h1>Welcome to TheCodeForge</h1>
<p>Your source for modern React tutorials.</p>
</main>
);
};
exportdefaultHome;
Keep page components pure — no NavLink or useNavigate inside them unless necessary. Let the router handle navigation.
🎯 Key Takeaway
One component per page file improves team collaboration and route debugging.
Making the Conclusion Actionable for Route Builders
React Router 6+ rewards a component-first mindset. By separating route config from page implementation, you get a codebase where adding a new page means creating one file and one route — nothing more. Lazy loading, protected routes, and error boundaries slot in without rewriting existing pages. The pattern is stable: every route gets a dedicated page component, routes are declared in a single location, and navigation stays decoupled from view logic. When your team encounters a routing bug, they can isolate it: is the route path wrong, or is a page component breaking? This separation slashes debugging time. Move forward by organizing your pages folder first, then wiring routes. The architecture you build today prevents tangled redirects and lost context tomorrow. Keep routes lean and pages focused — that’s the lasting takeaway.
Don’t embed route logic inside page components. A page should render UI; the router decides when to render it.
🎯 Key Takeaway
Decouple page components from routing config to future-proof your app structure.
React Router v7 Updates
React Router v7 introduces significant enhancements focused on type safety, data loading, and developer experience. The new loaders and actions are now fully typed, allowing you to define expected parameters and return types using TypeScript generics. For example, a loader can be typed as LoaderFunctionArgs to ensure type-safe access to route params. Actions similarly benefit from typed form data and responses. Additionally, v7 stabilizes the useFetcher hook for imperative data mutations and introduces defer for streaming data. The createBrowserRouter API now supports HydrateFallback for SSR hydration, and the RouterProvider component enables more flexible rendering. These updates make React Router a robust framework for data-intensive applications, reducing boilerplate and runtime errors.
Adopt v7's typed loaders to reduce runtime errors in data fetching, especially in large teams where API contracts change frequently.
🎯 Key Takeaway
React Router v7 brings full type safety to loaders and actions, improving code reliability and developer productivity.
Comparison: React Router vs TanStack Router
Both React Router and TanStack Router are powerful data-loading routers, but they differ in philosophy and features. React Router (v6+) focuses on a declarative, component-based approach with nested routes and data loaders integrated into the route config. TanStack Router emphasizes type safety and fine-grained control, using a schema-based route definition with automatic type inference. For example, TanStack Router generates TypeScript types for route parameters and search params automatically, while React Router requires manual typing. TanStack Router also supports file-based routing out of the box, whereas React Router relies on manual configuration. In terms of performance, TanStack Router uses a virtual router that minimizes re-renders, while React Router's data API is built on top of the existing React context. For complex applications with many routes and strict type requirements, TanStack Router may offer better developer experience. However, React Router has a larger ecosystem and simpler learning curve. Choose based on your team's familiarity and project needs.
Evaluate both routers in a prototype before committing; TanStack Router's type safety can reduce bugs in large codebases, but React Router's maturity may be safer for critical systems.
🎯 Key Takeaway
React Router offers ease of use and ecosystem, while TanStack Router provides superior type safety and performance for complex apps.
thecodeforge.io
React Router
Lazy Loading Routes with React.lazy + Suspense
Lazy loading routes improves initial load time by splitting your bundle into smaller chunks that load on demand. React Router supports this via React.lazy and Suspense. Define a lazy component for each route and wrap the route element in a Suspense boundary with a fallback UI. For example, const Blog = React.lazy(() => import('./routes/Blog')); then use Loading...
}>} />. This pattern works seamlessly with React Router's data loaders; the loader runs before the component loads, so you can show a loading spinner during both data fetch and component download. For optimal performance, combine lazy loading with route-based code splitting using tools like Webpack or Vite. Ensure that each lazy route is a separate chunk by using dynamic imports. Avoid lazy loading very small components to prevent overhead. This technique can reduce initial bundle size by up to 60% in large applications.
Use route-level code splitting to achieve 50-60% reduction in initial bundle size; monitor chunk sizes to ensure each lazy route is substantial enough to justify the split.
🎯 Key Takeaway
Lazy loading routes with React.lazy and Suspense reduces initial bundle size, improving load times for large apps.
● Production incidentPOST-MORTEMseverity: high
Infinite Redirect Loop on Auth Guard
Symptom
After successful login, user is redirected to /dashboard, but immediately sent back to /login with no error message. The browser back button loops through the same redirects. Console shows no errors, but network tab shows multiple 302 redirects.
Assumption
The team assumed that setting an auth token in localStorage and checking its existence in the protected route component would be sufficient. They also assumed <Navigate replace /> would prevent history pollution.
Root cause
The protected route was checking localStorage token on every render, but the login page also used the same token check. After login, the token was set but the protected route re-rendered before the auth context updated state asynchronously. Because the check was synchronous, the route saw no token and redirected back to login, which then saw the token and tried to redirect forward — creating an infinite loop. Additionally, the redirect used replace: true which replaced the current history entry, making the loop invisible in the browser's back button.
Fix
Replace synchronous localStorage check with an asynchronous auth context that provides a loading state. In the protected route, show a spinner while auth state resolves. Only redirect after the context confirms unauthenticated status. Use a useEffect to handle the redirect, not during render. Also add a flag to prevent redirect if the user is already on the login page.
Key lesson
Never trust synchronous storage reads for auth state — async context prevents race conditions.
Always show a loading state while auth resolves; redirect only after confirmation.
Use a dedicated auth provider that emits authenticated/unauthenticated as a state, not a side effect.
Avoid using replace in redirects during auth flows — it hides the loop from the user.
Production debug guideSymptoms and actions for the most common routing failures5 entries
Symptom · 01
Page renders blank or 404 on refresh in production
→
Fix
Check server configuration for URL rewriting. For Nginx: try_files $uri /index.html;. For Netlify: add /* /index.html 200 to _redirects.
Symptom · 02
Component does not re-fetch data when URL param changes
→
Fix
Verify that the useEffect dependency array includes the param from useParams(). Without it, the effect runs only once and shows stale data.
Symptom · 03
User redirected to login repeatedly after authenticating
→
Fix
Check if protected route component uses synchronous storage check. Refactor to async auth context with a loading state before rendering the route.
Symptom · 04
Nested route does not render inside parent layout
→
Fix
Ensure parent route's element includes <Outlet />. If using v5, check for missing exact prop. In v6, verify child routes are defined inside the parent <Route> block.
Symptom · 05
NavLink active class not applied correctly
→
Fix
Use the className callback with isActive parameter. Check that the paths match exactly — trailing slashes matter. For partial matching, use end prop on NavLink.
★ React Router Quick Debug Cheat SheetFast commands and checks for the most common routing problems in production
Blank page on route−
Immediate action
Open browser DevTools Console tab and look for React Router errors or missing context.
Commands
console.log(window.location.pathname); // Check current URL
Set breakpoint in your route config to inspect whether the route definition is correct.
Fix now
Wrap your app in <BrowserRouter> and ensure <Routes> is present. Add a catch-all route with path="*" to debug unmatched paths.
Data not updating when navigating to same route with different param+
Immediate action
Check the useEffect dependency array in the component that fetches data.
Commands
console.log(useParams()); // Shows current param values
console.log('Effect ran'); inside the useEffect to see if it triggers on navigation.
Fix now
Add the param(s) from useParams() into the dependency array of the data-fetching useEffect.
Protected route shows protected content before redirecting+
Immediate action
Check that the auth check is synchronous and happens before rendering Outlet.
Commands
Check React DevTools Components tab — the protected route component should not mount child routes if user is not authenticated.
Add a console.log in the protected route's render to see order of execution.
Fix now
Use a wrapper component that returns <Navigate> if not authenticated, <Outlet> otherwise. Do not use useNavigate() during render.
Making the Conclusion Actionable for Route Builders
routesproduct.tsx
interface Product {
React Router v7 Updates
routestanstack-example.tsx
const productRoute = new Route({
Comparison
App.tsx
const Home = lazy(() => import('./routes/Home'));
Lazy Loading Routes with React.lazy + Suspense
Key takeaways
1
React Router intercepts browser navigation using the History API
no server round-trips, no page reloads, your React tree stays alive the whole time.
2
useParams() always returns strings
parse them explicitly before passing to APIs or numeric comparisons, or you'll chase subtle bugs that only appear in production.
3
The <Outlet /> + parent route pattern is how you build shared layouts (navbars, sidebars, footers) without duplicating JSX across every page
this is the pattern React Router was designed around.
4
Use <Navigate /> (declarative) for auth guard redirects during render, and useNavigate() (imperative) only inside event handlers and useEffect callbacks
mixing them up is the most common intermediate-level React Router mistake.
5
Programmatic navigation via useNavigate() should never be called during render
it's a side effect meant for event handlers and async callbacks.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What's the difference between and in React Router, and ...
Q02SENIOR
How would you implement a route that redirects unauthenticated users to ...
Q03SENIOR
If a user navigates from /products/1 to /products/2 and the new product ...
Q04SENIOR
Explain how React Router handles nested layouts with and how yo...
Q01 of 04JUNIOR
What's the difference between and in React Router, and when would you choose one over the other in a production app?
ANSWER
<Link> is the basic component for navigation. It renders an anchor tag and navigates to the given to path without a page reload. You use it for any link where you don't need to style the current page differently — back buttons, sidebar links that always look the same, etc.
<NavLink> extends <Link> and automatically adds an isActive boolean to its className and style callbacks. This makes it perfect for navigation menus where the current page should be visually highlighted. For example, in a sidebar with links to /dashboard, /orders, /settings, NavLink lets you easily apply an 'active' class to the current section.
In production, use <Link> for generic navigation (breadcrumbs, 'go back', external links) and <NavLink> for primary navigation menus that need active state styling. Overusing NavLink when you don't need active detection adds unnecessary re-renders — each NavLink subscribes to location changes.
Q02 of 04SENIOR
How would you implement a route that redirects unauthenticated users to a login page and then sends them back to their originally requested URL after they log in?
ANSWER
Create a ProtectedRoute wrapper component that reads auth state from context. If the user is not authenticated, render <Navigate to="/login" state={{ redirectAfterLogin: currentLocation.pathname }} replace />. The state carries the URL they tried to access. On the login page, read location.state?.redirectAfterLogin and after successful authentication, call navigate(redirectTarget, { replace: true }). This ensures the user ends up exactly where they wanted, not a generic home page. Use replace to avoid history pollution.
Q03 of 04SENIOR
If a user navigates from /products/1 to /products/2 and the new product doesn't load, what are the two most likely causes and how would you debug them?
ANSWER
Cause 1: Missing dependency in useEffect. The data-fetching effect didn't include productId from useParams() in its dependency array. Navigation changes the URL but the effect never re-runs, showing stale data. Debug: add a console.log inside the effect — if it runs only once, dependencies are wrong.
Cause 2: useParams() returns string but API expects number. The param '2' is passed to an endpoint expecting a number, causing a 400 or wrong data. Debug: check the network tab for the actual request payload. Fix by parsing with Number(productId).
Additionally, ensure the component re-renders on param change — add a key prop if needed.
Q04 of 04SENIOR
Explain how React Router handles nested layouts with and how you would pass context to child routes.
ANSWER
React Router's <Outlet /> is a placeholder inside a parent route's element. When a child route matches, the child's element renders inside the parent's Outlet. This allows the parent to provide persistent UI without duplication.
Pass context using the context prop: <Outlet context={{ user, cartCount }} />. Child routes read it with useOutletContext(). This avoids prop drilling.
Production tip: memoize the context value if it changes frequently to prevent unnecessary re-renders.
01
What's the difference between and in React Router, and when would you choose one over the other in a production app?
JUNIOR
02
How would you implement a route that redirects unauthenticated users to a login page and then sends them back to their originally requested URL after they log in?
SENIOR
03
If a user navigates from /products/1 to /products/2 and the new product doesn't load, what are the two most likely causes and how would you debug them?
SENIOR
04
Explain how React Router handles nested layouts with and how you would pass context to child routes.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between BrowserRouter and HashRouter in React Router?
BrowserRouter uses the HTML5 History API and produces clean URLs like /products/1. HashRouter puts a # in the URL (/#/products/1) and stores the route in the URL hash, which never gets sent to the server. Use BrowserRouter for any app deployed to a proper server or CDN that supports URL rewriting. Use HashRouter only for static file hosting (like GitHub Pages) where you can't configure server-side URL rewrites, because the hash portion is guaranteed to stay client-side.
Was this helpful?
02
How do I pass data between routes in React Router without putting it in the URL?
Use the state option on navigate() or the state prop on <Link>. For example: navigate('/order-confirmation', { state: { orderId: 99 } }). The receiving page reads it with const location = useLocation(); const { orderId } = location.state;. Be aware that location state is ephemeral — it disappears on a hard refresh, so never rely on it as a primary data source. It's best for transient UI state like 'show a success toast' or 'pre-fill a form field'.
Was this helpful?
03
Why does my app show a blank page or 404 when I refresh on a React Router route in production?
This is the most common React Router deployment gotcha. When you refresh /products/42, the browser sends a real HTTP request for /products/42 to your server. Your server has no file at that path — only index.html exists — so it returns a 404. The fix is to configure your server to serve index.html for all routes and let React Router handle the rest client-side. In Nginx this is try_files $uri /index.html;, in Apache it's a .htaccess rewrite rule, and in Vite/Create React App deployments you set this in your hosting config (Netlify uses a _redirects file with /* /index.html 200).
Was this helpful?
04
Can I use React Router with TypeScript? How do I type route params?
Yes, React Router has first-class TypeScript support. For typed route params, define a type for your params object and use a generic version of useParams. For example: type ProductParams = { productId: string; }; const { productId } = useParams<ProductParams>();. Note that type will not enforce that the param actually exists at runtime — it's only for compile-time safety. For more robust param handling, consider using a route loader with createBrowserRouter and typed responses.
Was this helpful?
05
How do I handle scroll restoration when navigating between pages?
React Router v6.4+ has built-in scroll restoration when using createBrowserRouter — it remembers scroll positions per route and restores them on navigation. If you're using <BrowserRouter>, you can implement scroll restoration manually using useLocation and useEffect to scroll to top on route change, or use the useScrollRestoration hook from some community packages. Additionally, you can set scrollBehavior: 'smooth' on anchor tags or use window.scrollTo(0, 0) in a useEffect that depends on the pathname.