grahamcrackers/skills

react-router

React Router v7 patterns for client-side routing including nested routes, loaders, actions, route params, search params, navigation guards, and code splitting.

First seen Feb 26, 2026

Installation

$ npx skills add grahamcrackers/skills --skill react-router

Summary

  • 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.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from grahamcrackers/skills · top by installs.

npx skills add grahamcrackers/skills

Browse all from grahamcrackers/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,943 B
  • docs SUMMARY.md 350 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 6 installs

SKILL.md

React Router v7 Best Practices

Modes

React Router v7 offers two modes:

Mode Use When
Declarative (SPA) Client-side only app, no SSR needed
Framework Full-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.