A loader is an async function on a route object. React Router 139,386 calls it with { request, params, context } the moment the route matches, waits for the promise, and only then renders the route's component, which reads the result with useLoaderData() — there is no loading state, because the component never exists without its data. params holds the URL parameters; request is a standard Request, so the query string is new URL(request.url).searchParams and cancellation is request.signal. Drop the routes below into the array from createBrowserRouter.
{ path: '/staff', Component: Layout, id: 'staff',
loader: () => fetch('/api/staff/summary').then((r) => r.json()),
children: [
{ path: ':id', Component: Person, loader: ({ params, request }) =>
fetch(`/api/staff/${params.id}`, { signal: request.signal })
.then((r) => r.json()) },
] }
const Layout = () => <><h2>{useLoaderData().count} people</h2><Outlet /></>;
const Person = () => <p>{useLoaderData().name}</p>; // only this route's dataData is scoped per route: Person sees only what :id returned and Layout only what /staff returned. To read another route's data, give that route an id — as above — and call useRouteLoaderData('staff').
A loader may also return a Response: redirect('/login') is followed before anything renders, and data(value, { status, headers }) attaches a status to a plain value. The json() helper was removed in React Router 7; return the object itself.
After any action the router re-runs every loader on the page; a route whose data cannot have changed opts out with shouldRevalidate, which receives currentParams, nextParams, formMethod and defaultShouldRevalidate. Loaders run in parallel across the matched chain, so a three-level tree costs one round trip, and a navigation that supersedes them aborts their request.signal — the race condition of Out-of-Order Responses, handled once.