Advanced Routing in React Router v6
React Router v6 completely revamped how routing works in React applications, introducing powerful data fetching patterns and simplified route definitions.
1. Nested Routes and Outlets
Nested routes allow you to render components inside other components based on the URL path. This is perfect for persistent layouts (like dashboards with sidebars).
The parent route component uses an <Outlet /> to dictate where the child route's UI should be injected.
import { Routes, Route, Outlet, Link } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="flex">
<nav>
<Link to="/dashboard/profile">Profile</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
<main>
{/* Child route components will render here! */}
<Outlet />
</main>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
{/* Index route renders at "/dashboard" exactly */}
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
2. Dynamic Params (useParams)
URL parameters allow you to capture dynamic segments of a route.
// Route definition
<Route path="/users/:userId" element={<UserProfile />} />
import { useParams } from 'react-router-dom';
function UserProfile() {
// Extracts the 'userId' from the URL
const { userId } = useParams();
return <div>Viewing Profile for User: {userId}</div>;
}
3. Search Parameters (useSearchParams)
Search parameters (query strings like ?sort=asc&page=2) are essential for pagination, filtering, and sorting without changing the core route. useSearchParams works similarly to React's useState.
import { useSearchParams } from 'react-router-dom';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort") || "popular";
const handleSortChange = (e) => {
// Merges new params, keeping others intact
setSearchParams(prev => {
prev.set("sort", e.target.value);
return prev;
});
};
return (
<div>
<select value={sort} onChange={handleSortChange}>
<option value="popular">Popular</option>
<option value="newest">Newest</option>
</select>
</div>
);
}
4. The Data Router: Loaders and Actions
The modern (and recommended) way to use React Router v6 is via createBrowserRouter. This enables the powerful Loader and Action APIs, moving data fetching into the router layer to prevent "render-then-fetch" waterfalls.
Loaders (Fetching Data)
Run before the route renders. The component only renders once the data is ready.
import { createBrowserRouter, RouterProvider, useLoaderData } from 'react-router-dom';
// 1. Define the loader
const userLoader = async ({ params }) => {
const res = await fetch(`/api/users/${params.userId}`);
if (!res.ok) throw new Error("User not found");
return res.json();
};
// 2. Consume the data in the component
function UserProfile() {
const user = useLoaderData();
return <h1>{user.name}</h1>;
}
// 3. Connect to router
const router = createBrowserRouter([
{
path: "/users/:userId",
element: <UserProfile />,
loader: userLoader, // Attach loader here
errorElement: <ErrorBoundary /> // Handles thrown errors from loaders!
}
]);
Actions (Mutating Data)
Actions handle form submissions and mutations natively.
import { Form, redirect } from 'react-router-dom';
// 1. Define the action
const createPostAction = async ({ request }) => {
const formData = await request.formData();
const title = formData.get("title");
await fetch("/api/posts", { method: "POST", body: JSON.stringify({ title }) });
return redirect("/dashboard"); // Redirect upon success
};
// 2. Trigger via <Form>
function CreatePost() {
return (
// <Form> prevents default and triggers the route's action
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
);
}
The loader/action pattern fundamentally simplifies React applications by removing the need for complex useEffect chains and global state for server data caching.