Getting started
Setup
npm install react-router
⚠️ In v7, import from react-router (not react-router-dom). DOM-specific APIs use react-router/dom.
v7 has 3 modes:
| Mode | Setup | Features |
|---|---|---|
| Declarative | <BrowserRouter> |
Basic routing |
| Data | createBrowserRouter() |
Loaders, actions, fetchers |
| Framework | Vite plugin | SSR, type safety, file routing |
Declarative Mode
import { BrowserRouter, Routes, Route } from "react-router";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Data Mode
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
const router = createBrowserRouter([
{
path: "/",
Component: Root,
ErrorBoundary: RootError,
children: [
{ index: true, Component: Home },
{ path: "about", Component: About },
],
},
]);
ReactDOM.createRoot(root).render(<RouterProvider router={router} />);
Router Types
| Router | Use Case |
|---|---|
BrowserRouter |
HTML5 history (declarative) |
HashRouter |
Hash-based URLs |
MemoryRouter |
Testing / non-browser |
createBrowserRouter() |
Data router (recommended) |
createHashRouter() |
Hash-based data router |
createMemoryRouter() |
Testing data router |
Routes
Nested Routes
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="concerts">
<Route index element={<ConcertsHome />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
</Route>
</Routes>
// Layout renders children via <Outlet />
import { Outlet } from "react-router";
function Layout() {
return (
<div>
<nav>...</nav>
<Outlet /> {/* Child routes render here */}
</div>
);
}
Route Types
// Dynamic segment
<Route path="users/:userId" element={<User />} />
// Optional segment
<Route path=":lang?/about" element={<About />} />
// Splat / catch-all
<Route path="files/*" element={<FileViewer />} />
// Index route (renders at parent's path)
<Route index element={<Home />} />
// Layout route (no path, groups children)
<Route element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Route>
// Lazy loading
<Route path="dashboard" lazy={() => import('./Dashboard')} />
Data Mode Route Objects
createBrowserRouter([
{
path: "/",
Component: Root,
ErrorBoundary: RootError,
children: [
{ index: true, Component: Home },
{ path: "users/:id", Component: User, loader: userLoader },
{ path: "settings", Component: Settings, action: settingsAction },
],
},
]);
Navigation
Link
import { Link } from 'react-router';
<Link to="/about">About</Link>
<Link to="/users/42">User 42</Link>
<Link to=".." relative="path">Back</Link>
NavLink
import { NavLink } from 'react-router';
// Auto .active class
<NavLink to="/about">About</NavLink>
// Custom className
<NavLink
to="/messages"
className={({ isActive, isPending }) =>
isActive ? 'text-red-500' : isPending ? 'text-gray-400' : ''
}
>
Messages
</NavLink>
// Style callback
<NavLink
to="/tasks"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
})}
>
Tasks
</NavLink>
Programmatic Navigation
import { useNavigate } from "react-router";
function LoginPage() {
const navigate = useNavigate();
async function handleLogin() {
await login();
navigate("/dashboard"); // Push
navigate("/dashboard", { replace: true }); // Replace
navigate(-1); // Go back
}
}
Redirect Component
import { Navigate } from "react-router";
<Route path="/old" element={<Navigate to="/new" replace />} />;
// In component
function ProtectedRoute({ children }) {
const auth = useAuth();
if (!auth.user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
Hooks
Navigation Hooks
import {
useParams,
useNavigate,
useLocation,
useSearchParams,
useMatch,
} from "react-router";
// Route params
const { userId } = useParams();
// Programmatic navigation
const navigate = useNavigate();
// Current location
const location = useLocation();
// location.pathname, .search, .hash, .state
// Query params
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("q");
setSearchParams({ q: "new" });
// Pattern matching
const match = useMatch("/users/:id");
Data Hooks
import {
useLoaderData,
useActionData,
useNavigation,
useRouteLoaderData,
useRevalidator,
} from "react-router";
// Data from route loader
const data = useLoaderData();
// Data from route action
const actionData = useActionData();
// Navigation state
const navigation = useNavigation();
// navigation.state: 'idle' | 'loading' | 'submitting'
// navigation.location, .formData, .formMethod
// Access parent/sibling route loader data
const rootData = useRouteLoaderData("root");
// Manual revalidation
const { revalidate } = useRevalidator();
Fetcher Hooks
import { useFetcher, useFetchers } from "react-router";
const fetcher = useFetcher();
// fetcher.state: 'idle' | 'loading' | 'submitting'
// fetcher.data, .formData, .formMethod
// fetcher.Form, .submit(), .load()
// All active fetchers
const fetchers = useFetchers();
Outlet Hooks
import { useOutletContext } from "react-router";
// Parent passes context
<Outlet context={{ user }} />;
// Child reads it
const { user } = useOutletContext();
Error Hooks
import { useRouteError, isRouteErrorResponse } from "react-router";
function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<h1>
{error.status} {error.statusText}
</h1>
);
}
return <h1>{error.message}</h1>;
}
Utility Hooks
import { useBlocker, useBeforeUnload } from "react-router";
// Block navigation (unsaved changes)
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
hasChanges && currentLocation.pathname !== nextLocation.pathname,
);
// blocker.state: 'unblocked' | 'blocked' | 'proceeding'
// blocker.proceed(), blocker.reset()
// Warn before tab close
useBeforeUnload((e) => e.preventDefault());
Data Loading
Loaders
// Route config
{
path: 'teams/:teamId',
Component: Team,
loader: async ({ params, request }) => {
const team = await fetchTeam(params.teamId);
if (!team) throw data('Not found', { status: 404 });
return { team };
},
}
// Component
import { useLoaderData } from 'react-router';
function Team() {
const { team } = useLoaderData();
return <h1>{team.name}</h1>;
}
Actions
{
path: 'projects/:id',
Component: Project,
action: async ({ params, request }) => {
const formData = await request.formData();
await updateProject(params.id, Object.fromEntries(formData));
return { ok: true };
},
}
Form Component
import { Form } from "react-router";
// Declarative form submission (calls route action)
<Form method="post" action="/projects/123">
<input name="title" />
<button type="submit">Save</button>
</Form>;
// Programmatic submission
import { useSubmit } from "react-router";
const submit = useSubmit();
submit(formData, { method: "post", action: "/logout" });
useFetcher
import { useFetcher } from "react-router";
function Newsletter() {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/subscribe">
<input name="email" />
<button type="submit">
{fetcher.state === "idle" ? "Subscribe" : "Saving..."}
</button>
</fetcher.Form>
);
}
Deferred Data
import { defer, Await } from "react-router";
import { Suspense } from "react";
// Loader
async function loader() {
return defer({
critical: await getCritical(),
slow: getSlowData(), // Don't await!
});
}
// Component
function Page() {
const { critical, slow } = useLoaderData();
return (
<div>
<h1>{critical.title}</h1>
<Suspense fallback={<Spinner />}>
<Await resolve={slow}>{(data) => <SlowSection data={data} />}</Await>
</Suspense>
</div>
);
}
Pending UI
import { useNavigation } from "react-router";
function GlobalSpinner() {
const navigation = useNavigation();
return navigation.state === "loading" ? <Spinner /> : null;
}
Error Handling
Error Boundaries
createBrowserRouter([
{
path: "/",
Component: Root,
ErrorBoundary: RootError, // Catches all descendant errors
children: [
{
path: "projects/:id",
Component: Project,
ErrorBoundary: ProjectError, // Catches project-specific errors
},
],
},
]);
// Throwing errors in loaders
import { data } from "react-router";
async function loader({ params }) {
const record = await db.get(params.id);
if (!record) throw data("Not found", { status: 404 });
return record;
}
v6 → v7 Migration
Key Changes
| Change | v6 | v7 |
|---|---|---|
| Package | react-router-dom |
react-router |
| DOM imports | from 'react-router-dom' |
from 'react-router/dom' (RouterProvider only) |
json() helper |
return json({ data }) |
return { data } (just return objects) |
defer() |
return defer({...}) |
Return raw objects |
| Future flags | Enable all v7_* flags before upgrading |
Flags become default behavior |
Future flags to enable in v6:
v7_relativeSplatPath, v7_startTransition, v7_fetcherPersist, v7_normalizeFormMethod, v7_partialHydration, v7_skipActionErrorRevalidation
Also see
- React Router docs (reactrouter.com)
- Upgrading from v6 (reactrouter.com)
- Data loading guide (reactrouter.com)