
React Impl Routing
- 13 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-impl-routing is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-impl-routing
- Frontend Development
- AI-coding skill
React Impl Routing by the numbers
- 13 all-time installs (skills.sh)
- Ranked #1,634 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-impl-routingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-impl-routing
Quick Reference
Router Setup (React Router 6.4+)
| Concept | API | Purpose |
|---|---|---|
| Router creation | createBrowserRouter() | Define route tree as object array |
| Router rendering | <RouterProvider router={router} /> | Mount the data router in React |
| Nested rendering | <Outlet /> | Render child route element inside parent layout |
| Navigation link | <Link to="/path"> | Client-side navigation without reload |
| Active link | <NavLink className={({isActive}) => ...}> | Link with active state styling |
| Programmatic nav | useNavigate() | Navigate from event handlers or effects |
| URL params | useParams() | Read dynamic route segments |
| Query strings | useSearchParams() | Read and update URL search parameters |
| Loader data | useLoaderData() | Access data returned by route loader |
| Action data | useActionData() | Access data returned by route action |
| Error info | useRouteError() | Access error thrown in loader/action/render |
Critical Warnings
NEVER use the legacy <BrowserRouter> + <Routes> pattern for new projects -- ALWAYS use createBrowserRouter with <RouterProvider>. The data router API enables loaders, actions, and lazy routes that the legacy API cannot support.
NEVER call navigate() during render -- ALWAYS call it inside useEffect, event handlers, or loader/action functions. Calling during render causes infinite re-render loops.
NEVER define route objects inside a component -- ALWAYS define routes at module scope or in a separate file. Defining inside a component recreates the router on every render, destroying all state.
NEVER use loader or action as async arrow functions that capture component scope -- loaders and actions run outside React component lifecycle. They receive {params, request} as arguments.
ALWAYS return or throw a Response or value from loaders and actions -- returning undefined causes runtime errors.
ALWAYS use <Form> from react-router-dom instead of <form> when you want route actions to handle submission -- native <form> bypasses the router entirely.
---
Route Configuration
createBrowserRouter Pattern
ALWAYS define routes as a configuration object array:
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <HomePage /> },
{
path: "projects",
element: <ProjectsLayout />,
children: [
{ index: true, element: <ProjectList /> },
{
path: ":projectId",
element: <ProjectDetail />,
loader: projectLoader,
action: projectAction,
errorElement: <ProjectError />,
},
],
},
{
path: "settings",
lazy: () => import("./routes/settings"),
},
],
},
]);
function App(): React.ReactElement {
return <RouterProvider router={router} />;
}Route Properties
| Property | Type | Purpose |
|---|---|---|
path | string | URL segment to match |
element | ReactElement | Component to render when matched |
errorElement | ReactElement | Fallback UI when loader/action/render throws |
loader | LoaderFunction | Fetch data before rendering |
action | ActionFunction | Handle form submissions / mutations |
lazy | () => Promise<RouteObject> | Code-split route module |
children | RouteObject[] | Nested child routes |
index | boolean | Default child route (renders in parent Outlet) |
---
Nested Routes and Layouts
Use <Outlet /> in parent routes to render matched child routes:
import { Outlet, NavLink } from "react-router-dom";
function RootLayout(): React.ReactElement {
return (
<div>
<nav>
<NavLink
to="/"
className={({ isActive }) => (isActive ? "active" : "")}
>
Home
</NavLink>
<NavLink
to="/projects"
className={({ isActive }) => (isActive ? "active" : "")}
>
Projects
</NavLink>
</nav>
<main>
<Outlet />
</main>
</div>
);
}Layout Route (pathless)
A route without a path serves as a layout wrapper without adding a URL segment:
{
element: <AuthenticatedLayout />,
children: [
{ path: "dashboard", element: <Dashboard /> },
{ path: "profile", element: <Profile /> },
],
}Index Route
An index route is the default child that renders when the parent path matches exactly:
{
path: "projects",
element: <ProjectsLayout />,
children: [
{ index: true, element: <ProjectList /> }, // matches /projects
{ path: ":id", element: <ProjectDetail /> }, // matches /projects/123
],
}---
Navigation
Link and NavLink
import { Link, NavLink } from "react-router-dom";
<Link to="/projects">Projects</Link>
<Link to="../settings">Settings</Link> {/* relative to current route */}
<NavLink
to="/projects"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
Projects
</NavLink>useNavigate
const navigate = useNavigate();
// In event handler or effect -- NEVER during render
navigate(`/projects/${id}`);
navigate("/login", { replace: true }); // replace history entry
navigate(-1); // go back
navigate("/dash", { state: { from: "/" } }); // pass state---
URL Parameters and Search Parameters
useParams
import { useParams } from "react-router-dom";
// Route: { path: "projects/:projectId/tasks/:taskId?" }
function TaskView(): React.ReactElement {
const { projectId, taskId } = useParams<{
projectId: string;
taskId?: string; // optional segment marked with ?
}>();
// ALWAYS check params exist -- useParams returns string | undefined
if (!projectId) throw new Error("projectId is required");
return <div>Project: {projectId}, Task: {taskId ?? "none"}</div>;
}useSearchParams
function ProjectList(): React.ReactElement {
const [searchParams, setSearchParams] = useSearchParams();
const filter = searchParams.get("filter") ?? "all";
const page = Number(searchParams.get("page") ?? "1");
const updateFilter = (newFilter: string): void => {
setSearchParams((prev) => {
prev.set("filter", newFilter);
prev.set("page", "1");
return prev;
});
};
return (
<div>
<button onClick={() => updateFilter("active")}>Active</button>
<button onClick={() => updateFilter("all")}>All</button>
<p>Filter: {filter}, Page: {page}</p>
</div>
);
}---
Data Loading
Route Loader
Loaders receive {params, request} and run before the route renders:
import { useLoaderData, type LoaderFunctionArgs } from "react-router-dom";
interface Project { id: string; name: string }
async function projectLoader({ params, request }: LoaderFunctionArgs): Promise<Project> {
const response = await fetch(`/api/projects/${params.projectId}`);
if (!response.ok) throw new Response("Project not found", { status: 404 });
return response.json();
}
function ProjectDetail(): React.ReactElement {
const project = useLoaderData() as Project;
return <h1>{project.name}</h1>;
}
// Register: { path: ":projectId", element: <ProjectDetail />, loader: projectLoader }Deferred Data with defer + Await
Use defer() to return a mix of awaited (critical) and deferred (non-critical) data. Render deferred promises with <Suspense> + <Await>:
export async function loader(): Promise<ReturnType<typeof defer>> {
const user = await fetchUser(); // critical -- await immediately
return defer({
user,
recommendations: fetchRecommendations(), // deferred -- NOT awaited
});
}
export function Component(): React.ReactElement {
const { user, recommendations } = useLoaderData() as {
user: User;
recommendations: Promise<Recommendation[]>;
};
return (
<div>
<h1>Welcome {user.name}</h1>
<Suspense fallback={<p>Loading...</p>}>
<Await resolve={recommendations}>
{(data: Recommendation[]) => <RecommendationList items={data} />}
</Await>
</Suspense>
</div>
);
}See references/examples.md for a full deferred loading example.
---
Route Actions
Form and Action Pattern
ALWAYS use <Form> from react-router-dom (not native <form>) to trigger route actions:
import { Form, useActionData, redirect, type ActionFunctionArgs } from "react-router-dom";
interface ActionErrors { name?: string }
async function createAction({ request }: ActionFunctionArgs): Promise<ActionErrors | Response> {
const formData = await request.formData();
const name = formData.get("name") as string;
if (!name || name.length < 3) return { name: "Name must be at least 3 characters" };
const project = await createProject({ name });
return redirect(`/projects/${project.id}`);
}
function NewProject(): React.ReactElement {
const errors = useActionData() as ActionErrors | undefined;
return (
<Form method="post">
<input name="name" type="text" />
{errors?.name && <span className="error">{errors.name}</span>}
<button type="submit">Create</button>
</Form>
);
}
// Register: { path: "new", element: <NewProject />, action: createAction }---
Lazy Routes
ALWAYS use lazy() for route-level code splitting on routes not needed at initial load:
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{
path: "admin",
lazy: () => import("./routes/admin"),
},
],
},
]);
// ./routes/admin.tsx -- MUST export named properties matching RouteObject
export async function loader(): Promise<AdminData> {
return fetchAdminData();
}
export function Component(): React.ReactElement {
const data = useLoaderData() as AdminData;
return <AdminPanel data={data} />;
}
// Optional: export errorElement, action, etc.The lazy() function MUST return an object with route properties (Component, loader, action, errorElement). It NEVER returns a default export -- use named exports matching the route property names.
---
Protected Routes
Loader-Based Protection (Preferred)
import { redirect, type LoaderFunctionArgs } from "react-router-dom";
function protectedLoader({ request }: LoaderFunctionArgs): null | Response {
const isAuthenticated = checkAuth();
if (!isAuthenticated) {
const url = new URL(request.url);
return redirect(`/login?returnTo=${url.pathname}`);
}
return null;
}
// Apply to route:
// { path: "dashboard", element: <Dashboard />, loader: protectedLoader }Wrapper Component Pattern
import { Navigate, Outlet, useLocation } from "react-router-dom";
function RequireAuth(): React.ReactElement {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
// Use as layout route:
// {
// element: <RequireAuth />,
// children: [
// { path: "dashboard", element: <Dashboard /> },
// { path: "settings", element: <Settings /> },
// ],
// }---
Error Handling
errorElement and useRouteError
import { useRouteError, isRouteErrorResponse } from "react-router-dom";
function RouteError(): React.ReactElement {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div><h1>{error.status}</h1><p>{error.statusText}</p></div>;
}
return <div><h1>Error</h1><p>{error instanceof Error ? error.message : "Unknown"}</p></div>;
}
// ALWAYS place errorElement on the root route as a catch-all.
// Place specific errorElement on child routes for granular error UIs.---
Decision Trees
Which Router Pattern?
New project?
├── YES → createBrowserRouter + RouterProvider (ALWAYS)
└── NO (legacy codebase with BrowserRouter)
├── Can migrate? → YES → migrate to createBrowserRouter
└── Cannot migrate yet → keep BrowserRouter, but do NOT add loaders/actionsWhere to Put Auth Check?
Need to redirect before any rendering?
├── YES → Use loader-based protection (redirect in loader)
└── NO (need access to React context like auth provider)
└── Use wrapper component pattern (<RequireAuth> with <Outlet>)How to Load Data?
Data needed before route renders?
├── YES → Use route loader
│ ├── All data critical? → await everything in loader
│ └── Some data non-critical? → use defer() + <Await>
└── NO (data loaded after user interaction)
└── Use useEffect or event handler in component---
Reference Links
- references/examples.md -- Complete routing patterns with TypeScript
- references/api-table.md -- React Router hooks and components reference
- references/anti-patterns.md -- Common routing mistakes and fixes
- references/api-table.md#official-sources -- Official React Router documentation links
react-impl-routing -- Anti-Patterns
AP-01: Using Legacy BrowserRouter for New Projects
NEVER use <BrowserRouter> with <Routes> and <Route> in new projects.
// WRONG -- legacy pattern, no loader/action support
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App(): React.ReactElement {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}// CORRECT -- data router with full feature support
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
]);
function App(): React.ReactElement {
return <RouterProvider router={router} />;
}Why: The legacy API cannot use loader, action, lazy, defer, useFetcher, or useNavigation. These are the core features that make React Router v6.4+ powerful.
---
AP-02: Defining Router Inside a Component
NEVER create the router object inside a React component.
// WRONG -- router recreated every render, destroys all state
function App(): React.ReactElement {
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
]);
return <RouterProvider router={router} />;
}// CORRECT -- router defined at module scope
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
]);
function App(): React.ReactElement {
return <RouterProvider router={router} />;
}Why: createBrowserRouter creates a stateful router instance. Recreating it on every render resets all navigation state, loader caches, and pending navigations.
---
AP-03: Calling navigate() During Render
NEVER call navigate() in the component body or during render.
// WRONG -- causes infinite re-render loop
function Dashboard(): React.ReactElement {
const navigate = useNavigate();
const { user } = useAuth();
if (!user) {
navigate("/login"); // called during render!
}
return <div>Dashboard</div>;
}// CORRECT -- use Navigate component for render-time redirects
function Dashboard(): React.ReactElement {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return <div>Dashboard</div>;
}
// ALSO CORRECT -- use useEffect for side-effect navigation
function Dashboard(): React.ReactElement {
const navigate = useNavigate();
const { user } = useAuth();
useEffect(() => {
if (!user) navigate("/login", { replace: true });
}, [user, navigate]);
return <div>Dashboard</div>;
}
// BEST -- use loader-based redirect (no component render needed)
function dashboardLoader(): null | Response {
if (!checkAuth()) return redirect("/login");
return null;
}Why: navigate() triggers a state update. Calling it during render causes React to re-render, which calls navigate again, creating an infinite loop.
---
AP-04: Using useEffect for Data Fetching Instead of Loaders
NEVER fetch route data in useEffect when a loader is available.
// WRONG -- waterfall: render component, then fetch, then render again
function ProjectDetail(): React.ReactElement {
const { id } = useParams<{ id: string }>();
const [project, setProject] = useState<Project | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/projects/${id}`)
.then((r) => r.json())
.then(setProject)
.finally(() => setLoading(false));
}, [id]);
if (loading) return <Spinner />;
if (!project) return <NotFound />;
return <div>{project.name}</div>;
}// CORRECT -- loader fetches before render, no loading state needed
async function loader({ params }: LoaderFunctionArgs): Promise<Project> {
const response = await fetch(`/api/projects/${params.id}`);
if (!response.ok) throw new Response("Not found", { status: 404 });
return response.json();
}
function ProjectDetail(): React.ReactElement {
const project = useLoaderData() as Project;
return <div>{project.name}</div>;
}Why: Loaders run in parallel with route transitions, eliminating render-fetch waterfalls. The component receives data already loaded, removing the need for loading states and null checks.
---
AP-05: Using Native form Instead of Router Form
NEVER use <form> when you want the router to handle submission.
// WRONG -- bypasses router, triggers full page navigation
function CreateProject(): React.ReactElement {
return (
<form method="post" action="/projects/new">
<input name="name" />
<button type="submit">Create</button>
</form>
);
}// CORRECT -- router intercepts, calls route action, stays client-side
import { Form } from "react-router-dom";
function CreateProject(): React.ReactElement {
return (
<Form method="post">
<input name="name" />
<button type="submit">Create</button>
</Form>
);
}Why: Native <form> causes a full-page POST request. React Router's <Form> intercepts the submission, serializes form data into a Request, and passes it to the route's action function -- all client-side.
---
AP-06: Returning undefined from Loaders or Actions
NEVER forget to return a value from a loader or action.
// WRONG -- returns undefined, causes runtime error
async function loader({ params }: LoaderFunctionArgs) {
const response = await fetch(`/api/items/${params.id}`);
if (response.ok) {
return response.json();
}
// falls through to undefined when not ok!
}// CORRECT -- ALWAYS return or throw
async function loader({ params }: LoaderFunctionArgs): Promise<Item> {
const response = await fetch(`/api/items/${params.id}`);
if (!response.ok) {
throw new Response("Not found", { status: 404 });
}
return response.json();
}Why: React Router expects loaders and actions to return a value or throw. Returning undefined causes "Cannot read properties of undefined" errors when useLoaderData() or useActionData() attempts to access the result.
---
AP-07: Not Handling Errors with errorElement
NEVER leave routes without error boundaries.
// WRONG -- error in loader crashes entire app
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{ path: "projects/:id", element: <Project />, loader: projectLoader },
],
},
]);// CORRECT -- root errorElement catches unhandled errors,
// child errorElement provides granular recovery
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <RootError />, // catch-all
children: [
{
path: "projects/:id",
element: <Project />,
loader: projectLoader,
errorElement: <ProjectError />, // route-specific
},
],
},
]);Why: Without errorElement, an error in a loader, action, or component render bubbles up and crashes the entire application. ALWAYS place an errorElement on the root route at minimum.
---
AP-08: Using Lazy with Default Export
NEVER use default export in lazy route modules.
// WRONG -- lazy() does not use default export
// routes/admin.tsx
export default function Admin() { return <div>Admin</div>; }
// router config
{ path: "admin", lazy: () => import("./routes/admin") }
// Result: nothing renders because there is no `Component` export// CORRECT -- use named exports matching RouteObject properties
// routes/admin.tsx
export function Component() { return <div>Admin</div>; }
export async function loader() { return fetchAdminData(); }
// router config
{ path: "admin", lazy: () => import("./routes/admin") }Why: lazy() spreads the returned module's named exports onto the route object. It looks for Component, loader, action, ErrorBoundary, etc. A default export is ignored entirely.
---
AP-09: Accessing Component State in Loaders
NEVER try to access React state, context, or hooks in loaders/actions.
// WRONG -- loaders run outside React, cannot use hooks or context
async function loader(): Promise<Data> {
const { token } = useAuth(); // ERROR: hooks only work in components
return fetch("/api/data", { headers: { Authorization: token } });
}// CORRECT -- use non-React state management in loaders
async function loader({ request }: LoaderFunctionArgs): Promise<Data> {
const token = getTokenFromCookie(); // plain function, no React
if (!token) return redirect("/login");
const response = await fetch("/api/data", {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
}Why: Loaders and actions execute outside the React component tree. They have no access to React hooks, context, or component state. Use plain JavaScript functions, cookies, localStorage, or module-level singletons for data that loaders need.
---
AP-10: Mutating searchParams Directly
NEVER mutate the URLSearchParams object from useSearchParams without using the setter.
// WRONG -- mutating the object does not trigger navigation
function FilterBar(): React.ReactElement {
const [searchParams] = useSearchParams();
const handleFilter = (value: string): void => {
searchParams.set("filter", value); // mutates but does NOT update URL
};
return <button onClick={() => handleFilter("active")}>Active</button>;
}// CORRECT -- use the setter function
function FilterBar(): React.ReactElement {
const [searchParams, setSearchParams] = useSearchParams();
const handleFilter = (value: string): void => {
setSearchParams((prev) => {
prev.set("filter", value);
return prev;
});
};
return <button onClick={() => handleFilter("active")}>Active</button>;
}Why: URLSearchParams is a mutable object, but React Router only updates the URL when setSearchParams is called. Directly mutating the object silently does nothing visible.
react-impl-routing -- API Reference
Router Creation Functions
| Function | Signature | Purpose |
|---|---|---|
createBrowserRouter | (routes: RouteObject[], opts?: { basename?: string }) => Router | Create router using History API (production standard) |
createHashRouter | (routes: RouteObject[], opts?: { basename?: string }) => Router | Create router using hash URLs (legacy/static hosting) |
createMemoryRouter | (routes: RouteObject[], opts?: { initialEntries?: string[], initialIndex?: number }) => Router | Create in-memory router (testing, non-browser) |
---
Router Components
| Component | Props | Purpose |
|---|---|---|
<RouterProvider> | router: Router, fallbackElement?: ReactElement | Mount a data router in React tree |
<Outlet> | context?: unknown | Render matched child route element |
<Link> | `to: string \ | Partial<Path>, replace?: boolean, state?: any` |
<NavLink> | Same as Link + `className?: string \ | (props: { isActive, isPending }) => string, style?: ..., end?: boolean` |
<Navigate> | to: string, replace?: boolean, state?: any | Declarative redirect (renders nothing) |
<Form> | method?: string, action?: string, encType?: string, replace?: boolean | Form that triggers route action |
<ScrollRestoration> | getKey?: (location, matches) => string | Restore scroll position on navigation |
<Await> | resolve: Promise<T>, errorElement?: ReactElement, children: (data: T) => ReactElement | Render deferred data inside Suspense |
---
Hooks -- Navigation
| Hook | Signature | Purpose |
|---|---|---|
useNavigate | () => NavigateFunction | Programmatic navigation |
useLocation | () => Location | Current location object { pathname, search, hash, state, key } |
useHref | (to: string) => string | Resolve a relative path to absolute href |
useResolvedPath | (to: string) => Path | Resolve relative path to { pathname, search, hash } |
useNavigation | () => Navigation | Navigation state: `{ state: "idle" \ |
useRevalidator | () => { revalidate(), state } | Manually trigger loader revalidation |
NavigateFunction
type NavigateFunction = {
(to: string, options?: { replace?: boolean; state?: any }): void;
(delta: number): void; // navigate(-1) = back, navigate(1) = forward
};---
Hooks -- Route Data
| Hook | Signature | Purpose |
|---|---|---|
useParams | `<T extends Record<string, string \ | undefined>>() => T` |
useSearchParams | () => [URLSearchParams, SetURLSearchParams] | Read/write URL query string |
useLoaderData | () => unknown | Access current route loader return value |
useActionData | () => unknown | Access current route action return value |
useRouteLoaderData | (routeId: string) => unknown | Access loader data from a parent route by ID |
useMatches | () => UIMatch[] | All currently matched routes with data and handle |
useRouteError | () => unknown | Access error thrown in loader/action/render |
useFetcher | <T>() => Fetcher<T> | Call loaders/actions without navigation |
useFetchers | () => Fetcher[] | All active fetchers |
SetURLSearchParams
type SetURLSearchParams = (
nextInit: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),
navigateOpts?: { replace?: boolean; state?: any }
) => void;---
Hooks -- Form Handling
| Hook | Signature | Purpose |
|---|---|---|
useSubmit | () => SubmitFunction | Programmatically submit a form |
useFormAction | (action?: string) => string | Resolve form action URL |
SubmitFunction
type SubmitFunction = (
target: FormData | URLSearchParams | Record<string, string> | null,
options?: { method?: string; action?: string; encType?: string; replace?: boolean }
) => void;---
RouteObject Properties
| Property | Type | Description |
|---|---|---|
path | string | URL path segment. Supports :param (dynamic) and :param? (optional) |
index | boolean | Marks as index route (default child). Mutually exclusive with children |
element | ReactElement | Component to render when route matches |
Component | React.ComponentType | Alternative to element -- pass component reference |
errorElement | ReactElement | UI to render when loader/action/render throws |
ErrorBoundary | React.ComponentType | Alternative to errorElement -- pass component reference |
loader | `(args: LoaderFunctionArgs) => Promise<T> \ | T` |
action | `(args: ActionFunctionArgs) => Promise<T> \ | T` |
lazy | () => Promise<Partial<RouteObject>> | Code-split route module |
children | RouteObject[] | Nested child routes |
handle | unknown | Custom data accessible via useMatches() |
shouldRevalidate | (args: ShouldRevalidateFunctionArgs) => boolean | Control when loaders re-run |
id | string | Unique route identifier (auto-generated if omitted) |
---
LoaderFunctionArgs / ActionFunctionArgs
| Property | Type | Description |
|---|---|---|
params | Record<string, string> | URL dynamic segment values |
request | Request | Standard Fetch API Request object with URL, method, headers |
Accessing query parameters in a loader:
async function loader({ request }: LoaderFunctionArgs): Promise<Data> {
const url = new URL(request.url);
const query = url.searchParams.get("q");
// ...
}Accessing form data in an action:
async function action({ request, params }: ActionFunctionArgs): Promise<Response> {
const formData = await request.formData();
const name = formData.get("name") as string;
// ...
}---
Utility Functions
| Function | Signature | Purpose |
|---|---|---|
redirect | `(url: string, init?: number \ | ResponseInit) => Response` |
json | (data: T, init?: ResponseInit) => Response | Return JSON response (deprecated in v7, use plain return) |
defer | (data: Record<string, unknown>) => DeferredData | Return mix of awaited and deferred promises |
isRouteErrorResponse | (error: unknown) => error is ErrorResponse | Type guard for Response-based errors |
generatePath | (path: string, params: Record<string, string>) => string | Fill dynamic segments in path template |
matchPath | `(pattern: string, pathname: string) => PathMatch \ | null` |
matchRoutes | `(routes: RouteObject[], location: string) => RouteMatch[] \ | null` |
---
Fetcher API
useFetcher() returns a fetcher that can call loaders/actions without causing navigation:
| Property / Method | Type | Description |
|---|---|---|
fetcher.load(href) | (href: string) => void | Call a route loader |
fetcher.submit(data, opts) | SubmitFunction | Call a route action |
fetcher.data | `T \ | undefined` |
fetcher.state | `"idle" \ | "loading" \ |
fetcher.formData | `FormData \ | undefined` |
fetcher.Form | React.ComponentType | Form component bound to this fetcher |
function InlineDelete({ projectId }: { projectId: string }): React.ReactElement {
const fetcher = useFetcher();
const isDeleting = fetcher.state !== "idle";
return (
<fetcher.Form method="post" action={`/projects/${projectId}`}>
<input type="hidden" name="intent" value="delete" />
<button type="submit" disabled={isDeleting}>
{isDeleting ? "Deleting..." : "Delete"}
</button>
</fetcher.Form>
);
}---
Official Sources
- https://reactrouter.com/en/main/start/overview
- https://reactrouter.com/en/main/routers/create-browser-router
- https://reactrouter.com/en/main/route/route
- https://reactrouter.com/en/main/route/loader
- https://reactrouter.com/en/main/route/action
- https://reactrouter.com/en/main/route/lazy
react-impl-routing -- Examples
Full Application Router Setup
// src/router.tsx
import { createBrowserRouter } from "react-router-dom";
import { RootLayout } from "./layouts/RootLayout";
import { RootError } from "./errors/RootError";
import { HomePage } from "./pages/HomePage";
export const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <HomePage /> },
{
path: "projects",
lazy: () => import("./routes/projects"),
},
{
path: "projects/:projectId",
lazy: () => import("./routes/project-detail"),
},
{
path: "projects/new",
lazy: () => import("./routes/project-new"),
},
{
path: "admin",
lazy: () => import("./routes/admin"),
},
{
path: "login",
lazy: () => import("./routes/login"),
},
],
},
]);
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);---
Lazy Route Module
ALWAYS export named properties that map to RouteObject fields:
// src/routes/projects.tsx
import { useLoaderData, type LoaderFunctionArgs } from "react-router-dom";
interface Project {
id: string;
name: string;
status: "active" | "archived";
}
export async function loader({ request }: LoaderFunctionArgs): Promise<Project[]> {
const url = new URL(request.url);
const status = url.searchParams.get("status") ?? "active";
const response = await fetch(`/api/projects?status=${status}`);
if (!response.ok) {
throw new Response("Failed to load projects", { status: response.status });
}
return response.json();
}
export function Component(): React.ReactElement {
const projects = useLoaderData() as Project[];
return (
<div>
<h1>Projects</h1>
<ul>
{projects.map((project) => (
<li key={project.id}>
<Link to={project.id}>{project.name}</Link>
</li>
))}
</ul>
</div>
);
}
Component.displayName = "ProjectList";---
CRUD Route with Loader + Action
// src/routes/project-detail.tsx
import {
useLoaderData,
useActionData,
Form,
redirect,
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "react-router-dom";
interface Project {
id: string;
name: string;
description: string;
}
interface ActionResult {
errors?: { name?: string; description?: string };
}
export async function loader({ params }: LoaderFunctionArgs): Promise<Project> {
const response = await fetch(`/api/projects/${params.projectId}`);
if (!response.ok) {
throw new Response("Project not found", { status: 404 });
}
return response.json();
}
export async function action({
params,
request,
}: ActionFunctionArgs): Promise<ActionResult | Response> {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await fetch(`/api/projects/${params.projectId}`, { method: "DELETE" });
return redirect("/projects");
}
const name = formData.get("name") as string;
const description = formData.get("description") as string;
const errors: ActionResult["errors"] = {};
if (!name || name.length < 2) errors.name = "Name must be at least 2 characters";
if (Object.keys(errors).length > 0) return { errors };
await fetch(`/api/projects/${params.projectId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, description }),
});
return redirect("/projects");
}
export function Component(): React.ReactElement {
const project = useLoaderData() as Project;
const actionData = useActionData() as ActionResult | undefined;
return (
<div>
<h1>Edit: {project.name}</h1>
<Form method="post">
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" defaultValue={project.name} />
{actionData?.errors?.name && (
<p className="error">{actionData.errors.name}</p>
)}
</div>
<div>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
defaultValue={project.description}
/>
</div>
<button type="submit">Save</button>
</Form>
<Form method="post">
<input type="hidden" name="intent" value="delete" />
<button type="submit" className="danger">Delete</button>
</Form>
</div>
);
}
Component.displayName = "ProjectDetail";---
Protected Route with Auth Redirect
// src/routes/admin.tsx
import {
redirect,
useLoaderData,
type LoaderFunctionArgs,
} from "react-router-dom";
interface AdminData {
users: Array<{ id: string; email: string; role: string }>;
stats: { totalUsers: number; activeToday: number };
}
export async function loader({ request }: LoaderFunctionArgs): Promise<AdminData | Response> {
const token = getAuthToken();
if (!token) {
const url = new URL(request.url);
return redirect(`/login?returnTo=${url.pathname}`);
}
const response = await fetch("/api/admin/dashboard", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.status === 403) {
throw new Response("Forbidden", { status: 403 });
}
if (!response.ok) {
throw new Response("Failed to load admin data", { status: response.status });
}
return response.json();
}
export function Component(): React.ReactElement {
const { users, stats } = useLoaderData() as AdminData;
return (
<div>
<h1>Admin Dashboard</h1>
<div className="stats">
<p>Total users: {stats.totalUsers}</p>
<p>Active today: {stats.activeToday}</p>
</div>
<table>
<thead>
<tr><th>Email</th><th>Role</th></tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Component.displayName = "AdminDashboard";---
Deferred Loading with Suspense
// src/routes/dashboard.tsx
import { Suspense } from "react";
import {
defer,
Await,
useLoaderData,
type LoaderFunctionArgs,
} from "react-router-dom";
interface User {
id: string;
name: string;
}
interface Activity {
id: string;
message: string;
timestamp: string;
}
interface Notification {
id: string;
title: string;
read: boolean;
}
interface DashboardData {
user: User;
recentActivity: Promise<Activity[]>;
notifications: Promise<Notification[]>;
}
export async function loader({ request }: LoaderFunctionArgs) {
const user = await fetchUser(); // critical -- await immediately
return defer({
user,
recentActivity: fetchActivity(user.id), // deferred
notifications: fetchNotifications(user.id), // deferred
});
}
export function Component(): React.ReactElement {
const { user, recentActivity, notifications } = useLoaderData() as DashboardData;
return (
<div>
<h1>Welcome, {user.name}</h1>
<section>
<h2>Recent Activity</h2>
<Suspense fallback={<p>Loading activity...</p>}>
<Await
resolve={recentActivity}
errorElement={<p>Failed to load activity</p>}
>
{(activities: Activity[]) => (
<ul>
{activities.map((a) => (
<li key={a.id}>{a.message}</li>
))}
</ul>
)}
</Await>
</Suspense>
</section>
<section>
<h2>Notifications</h2>
<Suspense fallback={<p>Loading notifications...</p>}>
<Await
resolve={notifications}
errorElement={<p>Failed to load notifications</p>}
>
{(items: Notification[]) => (
<ul>
{items.map((n) => (
<li key={n.id} className={n.read ? "read" : "unread"}>
{n.title}
</li>
))}
</ul>
)}
</Await>
</Suspense>
</section>
</div>
);
}
Component.displayName = "Dashboard";---
Search Parameters with Pagination
import { useSearchParams, Link } from "react-router-dom";
interface PaginationProps {
totalPages: number;
}
function Pagination({ totalPages }: PaginationProps): React.ReactElement {
const [searchParams] = useSearchParams();
const currentPage = Number(searchParams.get("page") ?? "1");
const createPageUrl = (page: number): string => {
const params = new URLSearchParams(searchParams);
params.set("page", String(page));
return `?${params.toString()}`;
};
return (
<nav aria-label="Pagination">
{currentPage > 1 && (
<Link to={createPageUrl(currentPage - 1)}>Previous</Link>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Link
key={page}
to={createPageUrl(page)}
className={page === currentPage ? "active" : ""}
aria-current={page === currentPage ? "page" : undefined}
>
{page}
</Link>
))}
{currentPage < totalPages && (
<Link to={createPageUrl(currentPage + 1)}>Next</Link>
)}
</nav>
);
}---
Nested Layout with Breadcrumbs
import { Outlet, useMatches, Link } from "react-router-dom";
interface RouteHandle {
breadcrumb: string;
}
function BreadcrumbLayout(): React.ReactElement {
const matches = useMatches();
const breadcrumbs = matches
.filter((match) => (match.handle as RouteHandle)?.breadcrumb)
.map((match) => ({
path: match.pathname,
label: (match.handle as RouteHandle).breadcrumb,
}));
return (
<div>
<nav aria-label="Breadcrumb">
<ol>
{breadcrumbs.map((crumb, index) => (
<li key={crumb.path}>
{index < breadcrumbs.length - 1 ? (
<Link to={crumb.path}>{crumb.label}</Link>
) : (
<span aria-current="page">{crumb.label}</span>
)}
</li>
))}
</ol>
</nav>
<Outlet />
</div>
);
}
// Route config with handle for breadcrumbs:
// {
// path: "projects",
// element: <BreadcrumbLayout />,
// handle: { breadcrumb: "Projects" },
// children: [
// { index: true, element: <ProjectList />, handle: { breadcrumb: "All" } },
// { path: ":id", element: <ProjectDetail />, handle: { breadcrumb: "Detail" } },
// ],
// }