This tutorial covers the React Router v7+ data-router APIs used by modern React Router applications. The project currently uses React Router 8.3.1 and the declarative route configuration style.
React Router routes are declared in a route tree. The router creates the route objects, resolves parameters, runs loaders, and renders the matching component.
1. Creating a router
Use createBrowserRouter to create a router for browser navigation. The router configuration can contain route components, nested children, loaders, and error elements.
import { createBrowserRouter, RouterProvider } from 'react-router';
import { Layout } from './Layout';
import Home from './Home';
import { Products } from './Products';
const router = createBrowserRouter([
{
path: '/',
Component: Layout,
children: [
{ path: '/', Component: Home },
{ path: '/products', Component: Products },
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
The route component is declared with Component, not with an element property. The root route contains a Layout component and its child routes are rendered through <Outlet />.
2. Navigation with Link and NavLink
Use Link for normal navigation and NavLink when the link should reflect the current route.
import { Link, NavLink } from 'react-router';
export function MainNavigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
<NavLink
to="/products"
className={({ isActive, isPending }) =>
isPending ? 'pending' : isActive ? 'active' : ''
}
>
Products
</NavLink>
</nav>
);
}
The NavLink callback receives:
-
isActive: whether the current location matches the link -
isPending: whether a navigation to that link is in progress
3. Nested routes and layouts
A layout route can render a shared header or sidebar and then render the current page through <Outlet />.
import { Outlet } from 'react-router';
export function Layout() {
return (
<div>
<header>
<h1>My Application</h1>
</header>
<main>
<Outlet />
</main>
</div>
);
}
The route tree can then be structured as follows:
const router = createBrowserRouter([
{
path: '/',
Component: Layout,
children: [
{ index: true, Component: Home },
{ path: 'products', Component: Products },
],
},
]);
Using index: true makes the home page the index route without requiring a separate empty path. The parent route's <Outlet /> renders either Home or Products.
4. Programmatic navigation with useNavigate
Use useNavigate when navigation needs to be triggered by an event handler.
import { useNavigate } from 'react-router';
export function ProductButton() {
const navigate = useNavigate();
const handleClick = () => {
navigate('/products');
};
return <button onClick={handleClick}>View products</button>;
}
The navigation function accepts a route path. For relative navigation, the router uses the current route context.
5. Dynamic routes
A route parameter is declared with a colon in the route path.
const router = createBrowserRouter([
{
path: '/',
Component: Layout,
children: [
{ path: 'products/:productId', Component: Product },
],
},
]);
The URL /products/123 matches the route and provides the parameter productId.
Read the parameter with useParams inside the matched component:
import { useParams } from 'react-router';
export function Product() {
const { productId } = useParams();
return <div>Product ID: {productId}</div>;
}
useParams() returns a read-only object. The parameter value is a string because it comes from the URL.
6. Loaders
A loader runs before the route component renders. It receives the current request and route parameters.
import type { LoaderFunctionArgs } from 'react-router';
export async function productLoader({ params }: LoaderFunctionArgs) {
const productId = params.productId;
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Response('Product not found', {
status: 404,
statusText: 'Not Found',
});
}
return await response.json();
}
The loader is attached to the route:
const router = createBrowserRouter([
{
path: '/products/:productId',
Component: Product,
loader: productLoader,
},
]);
The route component reads the loader result with useLoaderData:
import { useLoaderData } from 'react-router';
export function Product() {
const product = useLoaderData();
return <div>{product.name}</div>;
}
The component is not rendered until the loader has resolved successfully. A rejected loader can therefore show an error element instead of rendering the product page.
useRouteLoaderData
useLoaderData() reads the data from the route that owns the component using it. If a child component needs data from a parent route's loader, use useRouteLoaderData() with the parent's route ID.
Give the parent route an explicit ID:
const router = createBrowserRouter([
{
path: '/products/:productId',
id: 'product-detail',
loader: productLoader,
children: [
{ path: 'edit', Component: ProductEdit },
{ path: 'display', Component: ProductDisplay },
],
},
]);
The parent route has no Component in this example. Its loader still runs, and the child routes can use the parent's loader data.
In a child component, request the parent's data by route ID:
import { useRouteLoaderData } from 'react-router';
export function ProductEdit() {
const product = useRouteLoaderData<{ name: string }>('product-detail');
if (!product) {
return <div>Product data is not available.</div>;
}
return <div>Edit product: {product.name}</div>;
}
The important difference is:
-
useLoaderData()reads the data for the route that owns the current component. -
useRouteLoaderData(routeId)reads the loader data for a specific named route, including a parent route.
The route ID must match the id assigned to the parent route. If the route has no explicit ID, React Router may generate an automatic route ID based on its route configuration, but an explicit ID makes the relationship clearer and more stable.
Returning a Response
A loader may return a Response directly. This can be useful when the loader needs to preserve an HTTP status or headers.
export async function productLoader() {
const response = await fetch('/api/products/123');
if (!response.ok) {
return new Response('Unable to load the product', {
status: response.status,
});
}
return response;
}
The returned Response is available through useLoaderData. For normal application data, returning parsed JSON is usually more convenient because the component can work with the resulting object directly.
7. Loader errors and errorElement
A loader can throw a Response to trigger an error element:
export async function productLoader({ params }: LoaderFunctionArgs) {
const response = await fetch(`/api/products/${params.productId}`);
if (!response.ok) {
throw new Response('Product not found', {
status: 404,
statusText: 'Not Found',
});
}
return await response.json();
}
Attach the error element to the route:
{
path: '/products/:productId',
Component: Product,
loader: productLoader,
errorElement: <ProductError />,
}
The error component can read the thrown value with useRouteError:
import { useRouteError } from 'react-router';
export function ProductError() {
const error = useRouteError();
return <div>{error instanceof Response ? error.status : 'Unknown error'}</div>;
}
useRouteError returns unknown, so the component should check the value before accessing properties such as status, statusText, or message.
8. Tracking navigation state with useNavigation
useNavigation returns information about the current navigation. Its state can be idle, loading, or submitting.
import { useNavigation } from 'react-router';
export function LoadingStatus() {
const navigation = useNavigation();
if (navigation.state === 'loading') {
return <div>Loading route...</div>;
}
if (navigation.state === 'submitting') {
return <div>Submitting data...</div>;
}
return null;
}
This hook is useful for showing a loading indicator or form-submission status. It does not replace the route loader's result; useLoaderData returns the resolved data, while useNavigation describes navigation state.
Summary
-
createBrowserRouterdefines the route tree. -
Linkperforms normal navigation. -
NavLinkprovides active and pending states. -
Outletrenders nested route components. -
useNavigateperforms navigation from event handlers. -
useParamsreads URL parameters. - A loader runs before its route component renders.
-
useLoaderDatareads the loader result. -
errorElementdisplays errors from loaders and route rendering. -
useRouteErrorreads the thrown error. -
useNavigationtracks whether navigation is loading or submitting.
Top comments (0)