NexusCS

React Router

React
React Router is the standard routing library for React. This guide covers React Router v7 (latest) with notes for v6 users.
featured

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