| name | react-impl-routing |
| description | Use when implementing client-side routing, setting up route configuration, handling navigation, or building protected routes. Prevents the common mistake of using legacy BrowserRouter instead of the data router API. Covers React Router v6+ createBrowserRouter, nested routes, Outlet, data loaders, actions, lazy routes, URL params, search params. Keywords: React Router, createBrowserRouter, Outlet, loader, action, route, protected route, login redirect, auth guard, private page, page navigation, nested routes..
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires React 18.x or 19.x with React Router 6.4+. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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 (): . {
;
}
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 /> },
{ path: ":id", element: <ProjectDetail /> },
],
}
Navigation
Link and NavLink
import { Link, NavLink } from "react-router-dom";
<Link to="/projects">Projects</Link>
<Link to="../settings">Settings</Link> {}
<NavLink
to="/projects"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
Projects
</NavLink>
useNavigate
const navigate = useNavigate();
navigate(`/projects/${id}`);
navigate("/login", { replace: true });
navigate(-1);
navigate("/dash", { state: { from: "/" } });
URL Parameters and Search Parameters
useParams
import { useParams } from "react-router-dom";
function TaskView(): React.ReactElement {
const { projectId, taskId } = useParams<{
projectId: string;
taskId?: string;
}>();
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>;
}
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();
return defer({
user,
recommendations: fetchRecommendations(),
});
}
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} />}
);
}
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 (
< =>
{errors?.name && {errors.name}}
Create
);
}
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"),
},
],
},
]);
export async function loader(): Promise<AdminData> {
return fetchAdminData();
}
export function Component(): React.ReactElement {
const data = useLoaderData() as AdminData;
return <AdminPanel data={data} />;
}
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;
}
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 />;
}
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>;
}
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/actions
Where 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