react-router

React Router v7 patterns for client-side routing including nested routes, loaders, actions, route params, search params, navigation guards, and code splitting. Use when setting up React Router, creating routes, handling navigation, loading route data, or when the user asks about React Router, SPA routing, or route configuration.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-router" with this command: npx skills add grahamcrackers/skills/grahamcrackers-skills-react-router

React Router v7 Best Practices

Modes

React Router v7 offers two modes:

ModeUse When
Declarative (SPA)Client-side only app, no SSR needed
FrameworkFull-stack with SSR, loaders, and actions (Remix-style)

This skill covers declarative/SPA mode. Use framework mode when you need SSR.

Setup (SPA Mode)

npm install react-router
import { BrowserRouter, Routes, Route } from "react-router";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route element={<Layout />}>
                    <Route index element={<Home />} />
                    <Route path="users" element={<Users />} />
                    <Route path="users/:userId" element={<UserDetail />} />
                    <Route path="*" element={<NotFound />} />
                </Route>
            </Routes>
        </BrowserRouter>
    );
}

Route Configuration

Object-Based Routes

import { createBrowserRouter, RouterProvider } from "react-router";

const router = createBrowserRouter([
    {
        element: <Layout />,
        errorElement: <ErrorPage />,
        children: [
            { index: true, element: <Home /> },
            { path: "users", element: <Users /> },
            {
                path: "users/:userId",
                element: <UserDetail />,
                loader: userLoader,
            },
        ],
    },
]);

function App() {
    return <RouterProvider router={router} />;
}

Object-based config with createBrowserRouter is recommended — it enables loaders, actions, and error boundaries.

Nested Routes and Layouts

<Route element={<DashboardLayout />}>
    <Route index element={<DashboardHome />} />
    <Route path="analytics" element={<Analytics />} />
    <Route path="settings" element={<Settings />} />
</Route>

The layout component renders an <Outlet /> where child routes appear:

function DashboardLayout() {
    return (
        <div>
            <DashboardSidebar />
            <main>
                <Outlet />
            </main>
        </div>
    );
}

Pathless Layout Routes

Wrap routes in a layout without adding a URL segment:

<Route element={<AuthLayout />}>
    {/* These routes require authentication */}
    <Route path="dashboard" element={<Dashboard />} />
    <Route path="profile" element={<Profile />} />
</Route>

Route Parameters

import { useParams } from "react-router";

function UserDetail() {
    const { userId } = useParams<{ userId: string }>();
    // fetch and render user
}

Optional and Catch-All

// Optional parameter
<Route path="users/:userId?" element={<Users />} />

// Catch-all / splat
<Route path="files/*" element={<FileViewer />} />

Access splat segments with useParams()["*"].

Search Params

import { useSearchParams } from "react-router";

function UserList() {
    const [searchParams, setSearchParams] = useSearchParams();
    const page = Number(searchParams.get("page") ?? "1");
    const sort = searchParams.get("sort") ?? "name";

    function setPage(newPage: number) {
        setSearchParams((prev) => {
            prev.set("page", String(newPage));
            return prev;
        });
    }

    return <List page={page} sort={sort} onPageChange={setPage} />;
}

Navigation

Links

import { Link, NavLink } from "react-router";

<Link to="/users/123">View User</Link>

<NavLink
  to="/dashboard"
  className={({ isActive }) => (isActive ? "active" : "")}
>
  Dashboard
</NavLink>

NavLink provides isActive and isPending states for styling active links.

Programmatic Navigation

import { useNavigate } from "react-router";

const navigate = useNavigate();

navigate("/users/123");
navigate("/users", { replace: true });
navigate(-1); // go back
navigate("/login", { state: { from: location.pathname } });

Redirects

import { Navigate } from "react-router";

<Route path="old-path" element={<Navigate to="/new-path" replace />} />;

Loaders

Fetch data before a route renders (requires createBrowserRouter):

async function userLoader({ params }: LoaderFunctionArgs) {
  const user = await api.users.getById(params.userId!);
  if (!user) throw new Response("Not Found", { status: 404 });
  return user;
}

// In route config
{ path: "users/:userId", element: <UserDetail />, loader: userLoader }

Access loader data in the component:

import { useLoaderData } from "react-router";

function UserDetail() {
    const user = useLoaderData() as User;
    return <div>{user.name}</div>;
}

Actions

Handle form submissions and mutations:

async function createUserAction({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await api.users.create(Object.fromEntries(formData));
  return redirect(`/users/${user.id}`);
}

// In route config
{ path: "users/new", element: <CreateUser />, action: createUserAction }
import { Form, useActionData, useNavigation } from "react-router";

function CreateUser() {
    const errors = useActionData() as ValidationErrors | undefined;
    const navigation = useNavigation();
    const isSubmitting = navigation.state === "submitting";

    return (
        <Form method="post">
            <input name="name" />
            {errors?.name && <span>{errors.name}</span>}
            <button type="submit" disabled={isSubmitting}>
                Create
            </button>
        </Form>
    );
}

Error Handling

{
  path: "users/:userId",
  element: <UserDetail />,
  loader: userLoader,
  errorElement: <UserError />,
}
import { useRouteError, isRouteErrorResponse } from "react-router";

function UserError() {
    const error = useRouteError();

    if (isRouteErrorResponse(error)) {
        return (
            <div>
                {error.status}: {error.statusText}
            </div>
        );
    }

    return <div>Something went wrong</div>;
}

Error boundaries catch errors from loaders, actions, and rendering. They bubble up to the nearest parent errorElement.

Code Splitting

import { lazy } from "react";

const Dashboard = lazy(() => import("./pages/dashboard"));

const router = createBrowserRouter([
    {
        path: "dashboard",
        element: (
            <Suspense fallback={<DashboardSkeleton />}>
                <Dashboard />
            </Suspense>
        ),
    },
]);

Protected Routes

function RequireAuth({ children }: { children: React.ReactNode }) {
    const { user } = useAuth();
    const location = useLocation();

    if (!user) {
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

// Usage
<Route
    path="dashboard"
    element={
        <RequireAuth>
            <Dashboard />
        </RequireAuth>
    }
/>;

Or with loaders:

function protectedLoader({ request }: LoaderFunctionArgs) {
    const user = getUser();
    if (!user) throw redirect("/login");
    return user;
}

Scroll Restoration

import { ScrollRestoration } from "react-router";

function Layout() {
    return (
        <>
            <Outlet />
            <ScrollRestoration />
        </>
    );
}

Place ScrollRestoration once in your root layout.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typescript-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code-principles

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-router

No summary provided by upstream source.

Repository SourceNeeds Review
General

bulletproof-react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review