Skip to main content

React Router

React Router is the standard routing library for React. It enables navigation between views, URL parameter handling, and keeps your UI in sync with the URL — all without page reloads. Real-world analogy: Think of React Router like a TV remote. In a traditional website, switching channels (pages) means turning off the TV, walking to the store to buy a new one, and turning it on again (a full page reload). With client-side routing, you press a button on the remote and the channel changes instantly — the TV (your browser) stays on, your app state is preserved, and only the content on screen swaps out.

Why Client-Side Routing?

Installation


Basic Setup

1. Wrap Your App with BrowserRouter

2. Define Routes

Common pitfall — using <a href> instead of <Link to>: A regular <a> tag causes the browser to make a full server request, which destroys all your React state and re-mounts the entire app. Always use React Router’s Link or NavLink for internal navigation.


Dynamic Routes (URL Parameters)

Defining Dynamic Routes

Accessing Parameters with useParams


Query Parameters (Search Params)


Programmatic Navigation

useNavigate Hook

Accessing Navigation State


Nested Routes

Create layouts with shared navigation:
The end prop on NavLink ensures it only matches exactly, not partial paths.

Protected Routes

Basic Protected Route

Analogy: A protected route is like a security checkpoint at an airport. If you have a valid boarding pass (auth token), you walk through to your gate (the protected page). If not, you are redirected to the ticketing counter (the login page), and the system remembers which gate you were trying to reach so it can send you there after you get your pass.

Using Protected Routes

Role-Based Protection


Lazy Loading Routes

Load routes only when needed:

Shared Suspense Boundary


useLocation Hook

Access current location information:

Scroll Restoration

Reset scroll position on navigation:

Error Boundaries with Routes


Common Router Pitfalls

Pitfall 1 — Stale data after route parameter changes: If you fetch data in a useEffect based on a URL parameter, make sure the parameter is in the dependency array. Otherwise, navigating from /users/1 to /users/2 will not re-fetch because the component stays mounted — React Router reuses the same component instance for the same route pattern.
Pitfall 2 — Missing key on route-level components: When the same component renders for different route params (e.g., /posts/1 and /posts/2), React reuses the component instance. If you need a full remount (reset form state, scroll to top, re-run all effects), add a key tied to the param:
Pitfall 3 — Broken back button with replace: Using navigate('/path', { replace: true }) everywhere makes the back button useless. Only use replace when going back to the previous page would be harmful (e.g., after login, after a form submission that should not be repeated).

🎯 Practice Exercises


Summary

Next Steps

In the next chapter, you’ll learn about Optimization & Deployment — making your React apps fast and production-ready!

Interview Deep-Dive

Strong Answer: Client-side routing uses the History API — window.history.pushState and the popstate event — to change the URL without triggering a full page reload. When you click a Link component, React Router calls history.pushState() to update the URL bar, then matches the new URL against your route definitions and renders the matching component. No HTTP request is sent to the server.The JavaScript bundle stays loaded, React’s component tree stays mounted (except for components that unmount due to route changes), and all client-side state is preserved. The popstate event fires when the user clicks the browser’s back/forward buttons. React Router listens for this and re-runs route matching.BrowserRouter uses clean URLs like /dashboard/settings. This requires server configuration: every URL must return the same index.html, because if a user refreshes on /dashboard/settings, the server receives that path and needs to serve the SPA’s entry point. Without this catch-all redirect, the server returns a 404. This is the most common deployment issue with SPAs.HashRouter uses the URL hash (/#/dashboard/settings) to avoid this problem because everything after # is not sent to the server. But hash URLs are ugly and not SEO-friendly.Follow-up: How would you implement code splitting per route, and what are the UX considerations for the loading state?Use React.lazy() with dynamic imports for each route component. Wrap the Routes in a Suspense boundary with a fallback loading UI. When the user navigates for the first time, the chunk is fetched, the Suspense fallback shows, and once loaded, the component renders.For instant-feeling navigation, prefetch route chunks on link hover so the chunk loads during the hover. For nested routes, place Suspense boundaries at each layout level so that navigating between sibling routes only shows the fallback in the content area, not the entire page.
Strong Answer: The architecture has three layers. First, a PrivateRoute wrapper that checks useAuth(). If not authenticated, it redirects to /login saving the attempted URL in location state. If authenticated, it renders Outlet.Second, a RoleRoute that checks user.role against allowedRoles and redirects to /unauthorized if the role does not match.Third, the login page reads location.state?.from and navigates there after successful login with replace: true.Edge cases from production: the loading state during async auth check must show a spinner, not flash the login page. Token expiry during a session means the next API call returns 401 and the fetch wrapper should clear auth state. Deep link sharing must preserve the full path including query params. The race condition on mount where the route renders before auth state resolves requires gating on loading === false.Follow-up: How do you handle authentication in Next.js differently from a client-side SPA?In a client-side SPA, auth checks happen after JavaScript loads. In Next.js with server rendering, auth checks happen on the server before HTML is sent. You read the auth cookie in a Server Component or middleware and redirect before the protected content reaches the client. This is faster and more secure. The tradeoff is requiring HTTP-only cookies instead of localStorage, which adds server-side cookie management complexity.
Strong Answer: These hooks represent three distinct parts of a URL. useParams reads path parameters for resource identifiers — the thing you are viewing. useSearchParams reads and writes query parameters for optional view configuration like filters, sort order, and pagination. useLocation gives the complete location object for analytics, breadcrumbs, or navigation state.The design principle: path params for identity (what resource), search params for view configuration (how to display it), location state for ephemeral navigation context (where you came from).A common mistake is putting filter state in component state instead of search params. If a user filters products and refreshes, their filters are lost. Search params make the URL the source of truth.Follow-up: How do you synchronize URL search params with React state without infinite loops?Treat the URL as the single source of truth. Read from useSearchParams directly — do not copy into useState. Update by calling setSearchParams in event handlers. This changes the URL, React Router re-renders, and the component reads new params. One render, one source of truth.The infinite loop trap: putting setSearchParams inside a useEffect that depends on search params creates a cycle. The effect sets params, triggering a re-render, which re-reads params (new reference), which re-triggers the effect. Always set params in event handlers, not effects.