web-routing-react-router-d860ce — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-routing-react-router-d860ce (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: This skill covers React Router v7 Data Mode —createBrowserRouterplus loaders, actions, fetchers and pending states, without a full framework. Four v7 facts change the answer: every export comes from"react-router", with the DOM-only ones —RouterProviderincluded — also on"react-router/dom", whilereact-router-domsurvives v7 as a deprecated re-export and is deleted in v8;json()anddefer()are removed so loaders return plain objects; form method values are uppercase ("POST"); and loaders skip revalidation after an action error unlessshouldRevalidateopts back in.
Detailed Resources:
<Form>, useFetcher, optimistic UI, shouldRevalidate, deferred dataLink, NavLink, useNavigate, redirect, useSearchParams wired to loadersroute.lazyOutlet, useOutletContext, pathless auth layouts, useBlockercreateBrowserRouter optionsReact Router ships three modes, and only one of them has the data APIs.
createBrowserRouter + <RouterProvider>. Loaders, actions, fetchers and errorElement all work. This is the skill; start at examples/core.md.<BrowserRouter> + <Routes>. URL matching and <Link> only. A loader prop here is silently ignored, so reach for it when there is no data to load and nothing in this skill's patterns applies.<critical_requirements>
Import everything from `"react-router"`, and `RouterProvider` from `"react-router/dom"` — that copy wires up react-dom's flushSync. react-router-dom still installs in v7 as a re-export, so a stale import fails no build and nothing tells you; v8 deletes the package.
Reach for `createBrowserRouter` + `<RouterProvider>` whenever a loader, action or fetcher is involved. Those are Data Mode features; under <BrowserRouter> the props are accepted and ignored, with no error to tell you.
Return plain objects from loaders, or a Response you built yourself. json() and defer() were removed in v7.
Use `throw redirect()` rather than `return redirect()`. Throwing unwinds the whole call stack, which is what makes a shared requireAuth() helper stop the loader that called it.
Give the root route an `errorElement` or `ErrorBoundary`. It is the last catch in the tree; without one, a single loader failure replaces the app with the router's built-in error dump.
</critical_requirements>
Auto-detection: createBrowserRouter, RouterProvider, useLoaderData, useActionData, useNavigation, useSearchParams, useFetcher, useRouteError, isRouteErrorResponse, useOutletContext, useRevalidator, useBlocker, Outlet, NavLink, errorElement, shouldRevalidate, route.lazy, HydrateFallback
Applies to:
Handled elsewhere:
NavLink hands its state to a className or style function, and what those return is not its concern.<philosophy>
React Router v7 treats the router as a data layer rather than a URL matcher. A route declares what to load (loader), what mutations it accepts (action), and what catches failures (errorElement) — all before its component renders. That moves orchestration out of components and removes the fetch-on-mount waterfall.
errorElement, leaving parent layouts on screen.</philosophy>
<decision_framework>
What does this code do?
+-- Fetch data the component needs? -> loader (parallel with sibling loaders)
+-- Handle a form submission? -> action (reads FormData, returns errors or redirects)
+-- Mutate without changing the URL? -> useFetcher (independent state, runs concurrently)
+-- Gate access to a subtree? -> pathless layout route whose loader throws redirect()Where are you?
+-- In JSX, on something clickable? -> <Link> or <NavLink>
+-- In a handler, after a side effect? -> useNavigate()
+-- In a loader or action? -> throw redirect()
+-- Outside the React tree entirely? -> router.navigate() on the router objectThe last one is the only route out of a module that never renders — a fetch wrapper redirecting on a 401, for instance. The hooks all need a component.
Put errorElement on the root for a guaranteed catch, and on individual routes where the failure has its own UI — a missing post reads differently from a failed dashboard. Throw a Response from a loader (throw new Response("Not Found", { status: 404 })) to make the failure an HTTP error that isRouteErrorResponse can narrow.
</decision_framework>
<patterns>
Routes are objects, defined outside React so the router can run loaders before anything mounts.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <HomePage /> },
{ path: "posts", element: <PostsPage />, loader: postsLoader },
],
},
]);Full code: examples/core.md
A loader runs before render; an action handles submissions. Both receive { request, params }, where request is a standard Web Request. Sibling loaders run in parallel, parent before child.
export async function postsLoader({ request }: LoaderFunctionArgs) {
const response = await fetch("/api/posts");
if (!response.ok) throw new Response("Failed to load", { status: 500 });
return { posts: await response.json() };
}
export async function createPostAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await createPost({ title: formData.get("title") });
throw redirect(`/posts/${post.id}`);
}Full code: examples/data-loading.md
errorElement catches failures from the route's loader, action and component alike. isRouteErrorResponse separates a thrown Response from an unexpected exception.
function RouteError() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <p role="alert">{error.status}: {error.statusText}</p>;
}
return <p role="alert">{error instanceof Error ? error.message : "Unknown error"}</p>;
}Full code: examples/error-handling.md
A parent route renders <Outlet /> where its matched child goes. The layout survives child navigation, so sidebars and their state persist.
function DashboardLayout() {
return (
<div>
<nav><NavLink to="/dashboard" end>Overview</NavLink></nav>
<main><Outlet /></main>
</div>
);
}Full code: examples/layouts.md
navigation.state is "idle", "loading" (a loader is running) or "submitting" (an action is). One indicator in the root layout covers the whole app.
function GlobalSpinner() {
const navigation = useNavigation();
if (navigation.state === "idle") return null;
return <div role="progressbar" aria-busy="true" />;
}Full code: examples/error-handling.md
fetcher.Form submits to an action without changing the URL. Fetchers carry independent state and data, several can be in flight at once, and active loaders still revalidate when each one finishes.
function DeleteButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="POST" action={`/posts/${postId}/delete`}>
<button type="submit" disabled={fetcher.state !== "idle"}>Delete</button>
</fetcher.Form>
);
}Full code: examples/data-loading.md
route.lazy defers a route module until navigation. The function form ships one chunk; the v7.5+ object form splits each property so the loader and the component download in parallel.
{ path: "admin", lazy: () => import("./pages/admin") }
{
path: "admin",
lazy: {
loader: async () => (await import("./pages/admin.loader")).loader,
Component: async () => (await import("./pages/admin.component")).AdminPage,
},
}Full code: examples/error-handling.md
</patterns>
<red_flags>
Breaks at runtime:
"react-router-dom" — deprecated in v7 and deleted in v8, and it still resolves today so nothing tells you — import from "react-router", or "react-router/dom" for RouterProvider.json() or defer() — removed in v7 — return a plain object, or a promise for the deferred half and read it with <Await>.loader or action on a <Route> under <BrowserRouter> — accepted and ignored with no warning — switch the tree to createBrowserRouter.errorElement anywhere in the tree — one loader failure replaces the app with the router's default error dump — put one on the root.return redirect() inside a shared helper — the caller carries on with a Response as its return value — throw it instead.<Outlet /> — matched children render nowhere, silently.useLoaderData() in a component whose route has no loader — returns undefined well away from the cause.navigation.formMethod === "post" — v7 uppercases form methods — compare against "POST".Surprising behaviour:
shouldRevalidate opts back in.useFetcher never moves useNavigation state; the two are independent.useSearchParams setter does not queue like setState — two calls in one tick do not build on each other, so use the callback form.route.lazy cannot supply path, index, children or id; those stay in the static config.params values are always strings — parse in the loader rather than in the component.navigate(-1) assumes a history entry that a deep-linked visitor does not have.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.