React Router: Params, Nested Routes & Loaders
Reviewed & published by Brayan K
You've seen basic routing — now let's go deeper. URL params let one route serve thousands of records (/users/:id). Nested routes with <Outlet /> share layout across pages. And loaders fetch a route's data before it renders, moving data fetching out of useEffect entirely. These are the patterns behind every real multi-page React app.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Define dynamic routes with :params and read useParams()
- Build nested routes and place an <Outlet />
- Use search params for sorting/filtering/pagination
- Fetch route data with loaders + useLoaderData()
- Why loaders beat useEffect for navigation data
- Handle missing data with errorElement
1️⃣ Dynamic Params
A colon marks a dynamic segment. One route, infinite records — read the value with useParams():
import { createBrowserRouter, useParams } from "react-router-dom";
const router = createBrowserRouter([
{ path: "/users/:id", element: <UserProfile /> },
]);
function UserProfile() {
const { id } = useParams(); // /users/42 -> { id: "42" }
return <h1>User #{id}</h1>;
}The demo models how the router turns a URL into that { id } object — including a non-match returning null:
// A route like "/users/:id" turns a URL into a params object.
// We model the matcher: split both paths and capture :segments.
function matchRoute(pattern, url) {
const pParts = pattern.split("/");
const uParts = url.split("/");
if (pParts.length !== uParts.length) return null;
const params = {};
for (let i = 0; i < pParts.length; i++) {
if (pParts[i].startsWith(":")) {
params[pParts[i].slice(1)] = uParts[i]; // capture the value
} else if (pParts[i] !== uParts[i]) {
return null; // a static part didn't match
}
}
return params;
}
console.log(matchRoute("/users/:id", "/users/42")); // { id: '42' }
console.log(matchRoute("/posts/:slug", "/posts/hello")); // { slug: 'hello' }
console.log(matchRoute("/users/:id", "/teams/42")); // null
// useParams() hands you exactly that captured { id } object.2️⃣ Nested Routes & Outlet
Children nest under a parent. The parent renders shared chrome and an <Outlet /> where the matched child appears — so the layout stays mounted as you navigate between children:
import { Outlet, NavLink } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/dashboard",
element: <DashboardLayout />, // persistent shell
children: [
{ index: true, element: <Overview /> }, // /dashboard
{ path: "settings", element: <Settings /> }, // /dashboard/settings
{ path: "billing", element: <Billing /> }, // /dashboard/billing
],
},
]);
function DashboardLayout() {
return (
<div className="dashboard">
<nav>
<NavLink to="/dashboard">Overview</NavLink>
<NavLink to="settings">Settings</NavLink>
<NavLink to="billing">Billing</NavLink>
</nav>
<main><Outlet /></main>
</div>
);
}For view options like sorting or pagination, use the query string via useSearchParams. Your turn — read the sort value from a parsed query:
import { useSearchParams } from "react-router-dom";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort") ?? "name"; // ?sort=price
return (
<button onClick={() => setSearchParams({ sort: "price" })}>
Sort by price (current: {sort})
</button>
);
}// 🎯 YOUR TURN — fill in the ___ then press "Try it Yourself".
// useSearchParams reads the query string after "?". Model it:
// parse "sort=price&page=2" into an object, then read one value.
function parseSearch(query) {
const params = {};
for (const pair of query.split("&")) {
const [key, value] = pair.split("=");
params[key] = value;
}
return params;
}
const sp = parseSearch("sort=price&page=2");
// 1) Read the "sort" value out of the parsed params.
console.log("sorting by:", sp[___]); // 👉 use "sort"
// ✅ Expected output:
// sorting by: price3️⃣ Loaders — Data Before Render
A loader runs during navigation, before the component mounts. The component reads the result with useLoaderData() — no in-component loading flag, no useEffect fetch:
The demo models the key idea: the loader runs first and produces ready data, then the component renders it:
// A loader fetches data BEFORE the component renders. We model the
// order: loader runs, returns data, THEN the component reads it ready.
function userLoader(id) {
const db = { 1: "Ada Lovelace", 2: "Linus Torvalds" };
// In React Router you'd return fetched data here.
return { id, name: db[id] ?? "Unknown" };
}
function UserPage(loaderData) {
// useLoaderData() gives you this object — already loaded, no spinner.
return "Profile: " + loaderData.name + " (#" + loaderData.id + ")";
}
// The router's job, in order:
const data = userLoader(2); // 1) loader runs during navigation
console.log(UserPage(data)); // 2) component renders with ready data
// Profile: Linus Torvalds (#2)Reorder Challenge
These lines of a route with a loader are scrambled. Put them in the correct order:
Quick Recall
1) The URL is /products/99 and the route is /products/:id. What does useParams() return?
{ id: "99" } — note params are always strings.
2) You navigate between two child routes of the same parent. Does the parent layout re-mount?
No. The parent stays mounted; only the <Outlet /> content swaps. That's the whole point of nesting.
3) Where does loader data come from inside the component?
useLoaderData() — the router resolves the loader before rendering and hands you the result.
Common Errors (and the fix)
- Treating params as numbers: params.id is a string. Use Number(id) if you need a number.
- Forgetting <Outlet />: child routes match but nothing renders. The parent must place an Outlet for children to appear.
- Absolute vs relative child paths: a child path: "settings" is relative; "/settings" is absolute and won't nest as you expect.
- Fetching in useEffect AND a loader: pick one. With the data router, prefer the loader and drop the effect.
📋 Quick Reference
| Task | Syntax |
|---|---|
| Dynamic route | path: "/users/:id" |
| Read params | const { id } = useParams() |
| Nested outlet | <Outlet /> |
| Search params | const [sp] = useSearchParams() |
| Loader data | const data = useLoaderData() |
Mini-Challenge: Build a Permalink
Routes go both ways: you read params from a URL, but you also build URLs to link to. Model a function that turns a post id into its permalink path /posts/:id.
// Build the URL for a post detail route from its id.
function postPath(id) {
// your code here: return "/posts/" + id
}
console.log(postPath(7));
console.log(postPath("welcome"));
// 🎯 Expected:
// /posts/7
// /posts/welcome🎉 Lesson Complete
- ✅ :param segments serve infinite records; read them with useParams()
- ✅ Nested routes share layout; <Outlet /> marks where children render
- ✅ Use search params for sorting/filtering/pagination
- ✅ Loaders fetch route data before render; read it with useLoaderData()
- ✅ errorElement catches loader failures cleanly
Practice quiz
How do you define a dynamic URL segment in React Router?
- With *id
- With {id}
- With a colon, like /users/:id
- With <id>
Answer: With a colon, like /users/:id. A colon marks a dynamic segment; /users/:id matches /users/42.
For the route /users/:id and URL /users/42, what does useParams() return?
- { id: "42" }
- { id: 42 }
- [42]
- "42"
Answer: { id: "42" }. useParams returns { id: "42" } — params are always strings.
What is <Outlet /> used for?
- Rendering a portal
- Reading query params
- Throwing route errors
- Marking where a matched child route renders inside a parent layout
Answer: Marking where a matched child route renders inside a parent layout. The parent places <Outlet /> where the matched child should appear, keeping the layout mounted.
Navigating between two child routes of the same parent…
- Re-mounts the parent layout
- Keeps the parent mounted; only the <Outlet /> content swaps
- Reloads the page
- Clears all state
Answer: Keeps the parent mounted; only the <Outlet /> content swaps. The parent stays mounted; only the Outlet content changes — the point of nested routes.
What does a route loader do?
- Fetches the route's data before the component renders
- Renders a spinner
- Caches components
- Handles form submits
Answer: Fetches the route's data before the component renders. A loader runs during navigation, before the component mounts, so data is ready on render.
How does a component read loader data?
- useParams()
- useState()
- useLoaderData()
- props.data
Answer: useLoaderData(). useLoaderData() returns the resolved loader result.
What's the difference between useParams and useSearchParams?
- They're identical
- useParams reads path segments; useSearchParams reads/writes the query string after ?
- useParams writes the URL; useSearchParams reads it
- useSearchParams is for nested routes
Answer: useParams reads path segments; useSearchParams reads/writes the query string after ?. Path params identify the resource; search params hold view options like sort/page.
Which renders if a route's loader throws?
- The nearest <Suspense>
- A blank page
- The parent layout
- The route's errorElement
Answer: The route's errorElement. errorElement catches loader failures cleanly for that route.
A child route with path: "settings" (no leading slash) is…
- An absolute path
- A relative path that nests under the parent
- Invalid
- The same as "/settings"
Answer: A relative path that nests under the parent. Without a leading slash it's relative and nests; "/settings" is absolute and won't nest as expected.
A key advantage of loaders over fetching in useEffect is…
- Less code only
- They run on the server only
- They eliminate the render → useEffect → spinner → re-render waterfall by loading before render
- They cache forever
Answer: They eliminate the render → useEffect → spinner → re-render waterfall by loading before render. The router resolves data during navigation, avoiding the in-component loading flash.
Continue this course
- Previous: Data Fetching with TanStack Query
- Next: Forms with React Hook Form + Zod — The current standard for performant forms with schema validation
- Quick reference: React cheat sheet