1. π Introduction
Almost every real-world React application needs more than one "page" β a home screen, a details view, a settings panel, and so on. React Router is the de-facto standard library for handling this kind of client-side navigation. This tutorial covers everything from basic setup to advanced data loading, protected routes, and performance techniques, using the modern RouterProvider-based API introduced in React Router v6.4+.
Information
2. β What is React Router?
React Router is a routing library for React that maps URL paths to components, enabling client-side navigation without full page reloads. It intercepts clicks on <Link> elements and browser history events, then swaps out rendered components based on the current URL.
3. π€ Why Use React Router?
- Single-page app navigation without full-page reloads, keeping app state intact
- Deep linking β users can bookmark or share a URL to a specific view
- Built-in support for data loading, error handling, and code splitting per route
- A large, mature ecosystem with strong TS support and community adoption
4. π¦ Installing React Router
terminal
npm install react-router-domNote
5. π BrowserRouter
<BrowserRouter> is the classic way to enable routing, using the HTTP History API to keep the UI in sync with the URL. It's simple to set up but doesn't support the newer data-loading features.
main.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}6. ποΈ RouterProvider
The modern, recommended approach uses createBrowserRouter to define routes as a configuration object, then renders them via <RouterProvider>. This unlocks loaders, actions, and better error handling.
main.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
]);
function App() {
return <RouterProvider router={router} />;
}7. βοΈ Route Configuration
Routes can be defined either declaratively with <Routes>/<Route> components, or as a configuration object passed to createBrowserRouter. Both describe the same underlying route tree.
DeclarativeRoutes.jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
</Routes>ObjectRoutes.jsx
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/products", element: <Products /> },
]);8. πΊοΈ Routes
<Routes> is the container component that scans its child <Route> elements and renders whichever one's path matches the current URL.
RoutesExample.jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>9. π£οΈ Route
Each <Route> maps a path pattern to an element. Paths can be static (/about), dynamic (/users/:id), or wildcard (*) for catch-all matching.
RouteExample.jsx
<Route path="/users/:userId" element={<UserProfile />} />10. π Link
<Link> renders an accessible <a> tag but intercepts the click to perform client-side navigation instead of a full page reload.
LinkExample.jsx
import { Link } from "react-router-dom";
function Nav() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
);
}11. π§ NavLink
<NavLink> is a special version of <Link> that automatically knows whether it's "active" β matching the current URL β and exposes this via a function-as-child or className callback.
NavLinkExample.jsx
<NavLink
to="/about"
className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")}
>
About
</NavLink>12. β‘οΈ Navigate
<Navigate> is a component that performs an imperative redirect as soon as it renders β useful for conditional redirects directly inside JSX.
NavigateExample.jsx
function LoginRedirect({ isAuthenticated }) {
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Dashboard />;
}13. πͺ useNavigate
useNavigate returns a function for programmatic navigation β for example, redirecting after a successful form submission.
UseNavigateExample.jsx
import { useNavigate } from "react-router-dom";
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
await login();
navigate("/dashboard");
};
return <form onSubmit={handleSubmit}>{/* fields */}</form>;
}14. πͺ Nested Routes
Nested routes mirror nested UI β a parent route renders shared layout, while child routes render inside it via an <Outlet> (see Section 26).
NestedRoutes.jsx
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="stats" element={<Stats />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>15. ποΈ Layout Routes
A layout route has no path of its own β it exists purely to wrap child routes in shared UI, like a header, sidebar, or footer.
LayoutRoute.jsx
<Route element={<AppLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Route>16. π Index Routes
An index route renders in place of its parent's <Outlet> when no child path is specified β essentially the "default" child view.
IndexRoute.jsx
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>17. π Dynamic Routes
Dynamic routes use a colon-prefixed segment (like :id) to match variable portions of a URL, such as a specific user or product.
DynamicRoute.jsx
<Route path="/products/:productId" element={<ProductDetail />} />18. π’ Route Parameters
Route parameters are the named, dynamic segments within a path β like productId in /products/:productId β and are read inside the matched component using useParams (see Section 21).
19. β Query Parameters
Query parameters appear after a ? in the URL (e.g. ?sort=price&page=2) and are typically used for filters, sorting, or pagination that shouldn't affect route matching.
20. π URL Search Parameters
React Router represents query parameters using the standard URLSearchParams web API, accessible via the useSearchParams hook.
21. πͺ useParams
useParams returns an object containing the dynamic segments matched by the current route.
UseParamsExample.jsx
import { useParams } from "react-router-dom";
function ProductDetail() {
const { productId } = useParams();
return <h1>Product #{productId}</h1>;
}22. πͺ useSearchParams
useSearchParams works like useState for the URL's query string β reading and updating parameters causes the URL (and history) to update accordingly.
UseSearchParamsExample.jsx
import { useSearchParams } from "react-router-dom";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort") ?? "newest";
return (
<select value={sort} onChange={(e) => setSearchParams({ sort: e.target.value })}>
<option value="newest">Newest</option>
<option value="price">Price</option>
</select>
);
}23. πͺ useLocation
useLocation returns the current location object β including pathname, search, hash, and any state passed during navigation.
UseLocationExample.jsx
import { useLocation } from "react-router-dom";
function PageTracker() {
const location = useLocation();
useEffect(() => {
trackPageView(location.pathname);
}, [location]);
return null;
}24. πͺ useMatch
useMatch checks whether a given path pattern matches the current URL, returning match details (like params) or null.
UseMatchExample.jsx
import { useMatch } from "react-router-dom";
function Breadcrumb() {
const match = useMatch("/products/:productId");
return match ? <span>Viewing product {match.params.productId}</span> : null;
}25. πͺ useMatches
useMatches (data router only) returns all matched routes for the current URL, including their loader data β commonly used to build dynamic breadcrumb trails.
UseMatchesExample.jsx
import { useMatches } from "react-router-dom";
function Breadcrumbs() {
const matches = useMatches();
return (
<nav>
{matches.filter((m) => m.handle?.crumb).map((m) => m.handle.crumb(m.data))}
</nav>
);
}26. π² Outlet
<Outlet> marks the spot within a parent (layout) route's JSX where the matched child route should render.
OutletExample.jsx
import { Outlet, Link } from "react-router-dom";
function DashboardLayout() {
return (
<div>
<nav>
<Link to="stats">Stats</Link>
<Link to="settings">Settings</Link>
</nav>
<main>
<Outlet />
</main>
</div>
);
}27. π Protected Routes
A protected route checks a condition β like authentication β before rendering its children, redirecting unauthorized users elsewhere.
ProtectedRoute.jsx
function ProtectedRoute({ isAuthenticated, children }) {
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
}
// Usage
<Route
path="/dashboard"
element={
<ProtectedRoute isAuthenticated={isLoggedIn}>
<Dashboard />
</ProtectedRoute>
}
/>28. π Authentication Routing
A common pattern wraps groups of protected routes in a single layout-style guard, rather than repeating the check on every route individually.
AuthLayout.jsx
function RequireAuth() {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
// Route config
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>Tip
29. π Role-Based Routing
Beyond simple authentication, some routes should only be accessible to users with a specific role (e.g. admin). Extend the guard component to check both authentication and authorization.
RequireRole.jsx
function RequireRole({ role, children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
if (user.role !== role) return <Navigate to="/unauthorized" replace />;
return children;
}30. π€ Lazy Loading Routes
Combining React.lazy with <Suspense> lets each route's code be downloaded only when the user actually navigates to it, reducing the initial bundle size.
LazyRoutes.jsx
import { lazy, Suspense } from "react";
const Settings = lazy(() => import("./Settings"));
<Route
path="/settings"
element={
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>
}
/>31. π₯ Route Loaders
A loader function fetches data before a route renders, so the component can access it synchronously via useLoaderData β no more manual useEffect + loading state boilerplate.
RouteLoader.jsx
const router = createBrowserRouter([
{
path: "/products/:productId",
element: <ProductDetail />,
loader: async ({ params }) => {
const res = await fetch(`/api/products/${params.productId}`);
return res.json();
},
},
]);
function ProductDetail() {
const product = useLoaderData();
return <h1>{product.name}</h1>;
}32. π Route Actions
An action function handles data mutations β typically form submissions β triggered via a router-aware <Form> component.
RouteAction.jsx
const router = createBrowserRouter([
{
path: "/products/new",
element: <NewProduct />,
action: async ({ request }) => {
const formData = await request.formData();
await createProduct(Object.fromEntries(formData));
return redirect("/products");
},
},
]);
function NewProduct() {
return (
<Form method="post">
<input name="name" />
<button type="submit">Create</button>
</Form>
);
}33. β³ Deferred Data
The defer utility lets a loader return immediately for critical data while slower data streams in later, rendered with <Await> and <Suspense>.
DeferredData.jsx
loader: async () => {
return defer({
critical: await fetchCriticalData(),
slowStats: fetchSlowStats(), // not awaited
});
};
function Page() {
const { critical, slowStats } = useLoaderData();
return (
<>
<CriticalView data={critical} />
<Suspense fallback={<Spinner />}>
<Await resolve={slowStats}>
{(stats) => <StatsView data={stats} />}
</Await>
</Suspense>
</>
);
}34. π§ Error Boundaries
Each route can define an errorElement that renders if the route's loader, action, or rendering throws β replacing the entire app crashing with a graceful fallback.
ErrorBoundary.jsx
{
path: "/products/:productId",
element: <ProductDetail />,
errorElement: <ProductError />,
loader: productLoader,
}
function ProductError() {
const error = useRouteError();
return <p>Something went wrong: {error.message}</p>;
}35. π§― Error Pages
A well-designed error page should explain what went wrong in plain language and offer a way back β such as a link to the homepage.
ErrorPage.jsx
function ErrorPage() {
const error = useRouteError();
return (
<div>
<h1>Oops!</h1>
<p>{error.statusText || error.message}</p>
<Link to="/">Go back home</Link>
</div>
);
}36. π³οΈ Not Found (404) Pages
A wildcard route with path="*" catches any URL that doesn't match a defined route, rendering a custom 404 page.
NotFound.jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>37. π Scroll Restoration
Data routers include a <ScrollRestoration> component that automatically resets scroll position on navigation, mimicking traditional multi-page browsing behavior.
ScrollRestorationExample.jsx
import { ScrollRestoration } from "react-router-dom";
function RootLayout() {
return (
<>
<Outlet />
<ScrollRestoration />
</>
);
}38. π§³ Navigation State
Both <Link> and navigate() accept a state option, letting you pass data to the next route without putting it in the URL.
NavigationState.jsx
navigate("/checkout", { state: { fromCart: true } });
// In the destination component
const location = useLocation();
console.log(location.state.fromCart); // trueCaution
39. π·οΈ Route Metadata
The handle property lets you attach arbitrary metadata β like page titles or breadcrumb labels β to a route, retrievable later via useMatches.
RouteMetadata.jsx
{
path: "/settings",
element: <Settings />,
handle: { crumb: () => <span>Settings</span> },
}40. ποΈ Route Organization
As an app grows, keep route definitions in a dedicated file (or split by feature) rather than inline in main.jsx, so the overall navigation structure stays easy to scan.
41. ποΈ Data Routers
"Data router" refers to routers created via createBrowserRouter (or createHashRouter, createMemoryRouter) that support loader, action, and errorElement β unlike the plain <BrowserRouter> from Section 5.
| Router Type | Supports loaders/actions? | Typical use |
|---|---|---|
| BrowserRouter | No | Simple apps, legacy code |
| createBrowserRouter | Yes | Modern apps, data loading |
| createMemoryRouter | Yes | Testing, non-browser environments |
42. π File-Based Routing Overview
Some meta-frameworks built on React Router β like Remix β infer routes automatically from the file system, rather than a manually written configuration array. A file named products.$productId.jsx automatically becomes the route /products/:productId.
Reference
43. π· TypeScript with React Router
React Router ships with TS types out of the box. Typing useParams and useLoaderData ensures your components can't accidentally reference a parameter or field that doesn't exist.
TypedRoute.tsx
interface Product {
id: string;
name: string;
}
function ProductDetail() {
const { productId } = useParams<{ productId: string }>();
const product = useLoaderData() as Product;
return <h1>{product.name}</h1>;
}44. β‘ Performance Optimization
- Use lazy loading (Section 30) so route bundles are only downloaded when needed
- Rely on loader data fetching rather than useEffect, avoiding a render-then-fetch waterfall
- Use defer for non-critical, slow-loading data so the rest of the page can render immediately
- Avoid unnecessary re-renders by memoizing layout components that wrap frequently-changing <Outlet> content
45. π Best Practices
- Prefer the RouterProvider + createBrowserRouter pattern for new projects
- Use loaders for data-fetching instead of component-level useEffect
- Group protected routes under a single layout-style guard rather than repeating checks
- Always provide an errorElement for routes that fetch data
- Keep route configuration organized in dedicated files as the app grows
46. β οΈ Common Mistakes
- Forgetting that <Outlet> is required for nested/child routes to render at all
- Using <a href> instead of <Link to>, causing an unwanted full page reload
- Placing the wildcard path="*" route before more specific routes, accidentally shadowing them
- Storing critical data only in navigation state, which disappears on refresh
- Mixing <BrowserRouter> usage with data-router-only hooks like useLoaderData
Danger
47. π¬ Frequently Asked Questions
Should I use BrowserRouter or createBrowserRouter for a new project?
For new projects, createBrowserRouter with <RouterProvider> is recommended β it unlocks loaders, actions, and better error handling that <BrowserRouter> doesn't support.
How do I redirect after a form submission?
Return a redirect(path) response from an action function, or call navigate(path) imperatively after an async operation completes inside an event handler.
Can I nest protected routes inside public ones?
Yes β wrap only the protected subset in a guard layout route (Section 28), while leaving sibling routes like a login or marketing page outside of it.