Routing (React Router)
React Router v6+: routes, nested outlets, hooks, data router loaders/actions, and protected routes.
Setup & basics#
React Router v6 matches the best route (not first-match, not exhaustive), supports relative nested routes, and replaced Switch/component/exact with Routes/element. Every path is exact by default.
import { BrowserRouter, Routes, Route, Link, NavLink } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
{/* NavLink gives you active state for styling */}
<NavLink to="/users" className={({ isActive }) => isActive ? "on" : ""}>
Users
</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<User />} />
<Route path="*" element={<NotFound />} /> {/* catch-all 404 */}
</Routes>
</BrowserRouter>
);
}
🎯 Link renders an <a> and does client-side navigation (no full reload). NavLink adds isActive/isPending for styling the current route.
Nested routes & <Outlet>#
A parent route renders shared chrome (layout, nav) and <Outlet /> marks where the matched child renders. index is the default child for the parent's exact path.
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} /> {/* /dashboard */}
<Route path="stats" element={<Stats />} /> {/* /dashboard/stats */}
<Route path="users/:id" element={<User />} /> {/* /dashboard/users/42 */}
</Route>
</Routes>
function DashboardLayout() {
return (
<div className="dash">
<Sidebar />
<Outlet /> {/* child route renders here */}
</div>
);
}
flowchart TD
A["URL /dashboard/users/42"] --> B["Match route tree"]
B --> C["DashboardLayout (parent)"]
C --> D["Sidebar + Outlet"]
D --> E["User (:id=42) renders in Outlet"]
Navigation & params hooks#
| Hook | Returns / does |
|---|---|
useNavigate() |
Imperative navigate: nav("/x"), nav(-1), nav("/x", { replace, state }) |
useParams() |
Dynamic segments object, e.g. { id: "42" } (always strings) |
useSearchParams() |
[params, setParams] over the query string (URLSearchParams) |
useLocation() |
Current location (pathname, search, state) |
useMatch(pattern) |
Match info for a pattern, or null |
function User() {
const { id } = useParams();
const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const tab = params.get("tab") ?? "profile";
return (
<>
<h1>User {id} — {tab}</h1>
<button onClick={() => setParams({ tab: "settings" })}>Settings</button>
<button onClick={() => navigate("/users", { replace: true })}>Back</button>
</>
);
}
⚠️ navigate from useNavigate is stable, but calling it during render throws — do it in effects or handlers. Params are strings; coerce (Number(id)) as needed.
Data router (loaders & actions)#
The data APIs (createBrowserRouter + RouterProvider) let routes fetch data before rendering (loaders) and handle form submissions (actions), enabling pending UI, deferred data, and error boundaries per route.
import { createBrowserRouter, RouterProvider, useLoaderData, redirect } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/users/:id",
element: <User />,
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
if (!res.ok) throw new Response("Not Found", { status: 404 });
return res.json(); // available via useLoaderData()
},
action: async ({ request, params }) => {
const form = await request.formData();
await fetch(`/api/users/${params.id}`, { method: "PUT", body: form });
return redirect(`/users/${params.id}`);
},
errorElement: <RouteError />,
},
]);
function Root() { return <RouterProvider router={router} />; }
function User() { const user = useLoaderData(); return <h1>{user.name}</h1>; }
🟢 Loaders remove render-then-fetch waterfalls and let you gate navigation. Use <Form> (not raw <form>) to trigger actions without a page reload. React Router v7 (Remix-merged) keeps these APIs.
Protected routes pattern#
Wrap children in a guard that redirects when unauthenticated. Preserve the attempted URL so you can return the user after login.
function RequireAuth({ children }) {
const user = useAuth();
const location = useLocation();
if (!user) return <Navigate to="/login" replace state={{ from: location }} />;
return children;
}
// usage
<Route path="/admin" element={
<RequireAuth><AdminPanel /></RequireAuth>
} />
With the data router, put the check in the loader and throw redirect("/login") — it runs before any component mounts, avoiding a flash of protected content. 🎯
Interview Q&A#
Q1. What changed from React Router v5 to v6?
Switch→Routes, component→element, routes are exact by default, relative nested routes with <Outlet>, and ranked best-match instead of first-match. v6.4 added the data router (loaders/actions).
Q2. Link vs NavLink vs useNavigate?
Link is a declarative client-side anchor. NavLink adds isActive/isPending for styling the current route. useNavigate navigates imperatively from handlers/effects.
Q3. What is <Outlet>?
A placeholder in a parent route's element where the matched child route renders — the mechanism for nested/layout routes and shared chrome.
Q4. Loaders vs fetching in useEffect?
Loaders run during navigation, before the component renders, eliminating render-then-fetch waterfalls and enabling pending states and per-route error boundaries. useEffect fetches only after mount, causing a loading flash.
Q5. How do you implement protected routes?
Wrap the element in a guard that returns <Navigate to="/login" replace state={{ from }} /> when unauthenticated, or throw redirect("/login") inside the route's loader for a flash-free redirect.