In React Router 7's data mode, what do a route's loader and action functions receive, and how does a component read what each returned?
answer
- declared on the route object
- a Fetch Request plus matched params
- before render versus on submission
- one hook named after each
basics
~20 sIn React Router 7, loader and action are route-object functions that each receive a Fetch Request and the matched params. The loader runs before the route renders and is read with useLoaderData; the action handles non-GET submissions and is read with useActionData.
solid answer
~40 sIn React Router 7's data mode (`createBrowserRouter` plus `<RouterProvider>`), `loader` and `action` are properties of a route object. Each receives one object with a Fetch `request` and the matched `params`; an action reads the submitted fields with `await request.formData()`. The router calls the matched loaders in parallel before it renders the route, so the component gets its data from `useLoaderData()` on the very first render, with no loading branch of its own. An action runs when a `<Form method="post">` or `useSubmit` targets the route; its return value is available through `useActionData()`, and afterwards the router revalidates the page's loaders. Pending state comes from `useNavigation()`, and because the request is real, the router aborts its `signal` when a navigation is interrupted.
code
ts · 23 linesimport { createBrowserRouter, data } from "react-router";
import { getProject, renameProject } from "./api";
import { ProjectPage } from "./ProjectPage";
export const router = createBrowserRouter([
{
path: "/projects/:projectId",
Component: ProjectPage,
loader: async ({ params, request }) => {
const project = await getProject(params.projectId!, { signal: request.signal });
return { project };
},
action: async ({ params, request }) => {
const form = await request.formData();
const title = String(form.get("title") ?? "").trim();
if (!title) {
return data({ error: "Title is required" }, { status: 400 });
}
await renameProject(params.projectId!, title);
return { error: null };
},
},
]);go deeper
Recall the pair: loader for reading before render, action for non-GET submissions, and the two hooks useLoaderData and useActionData that read them.
Explain the arguments, request and params, why the loader data exists on first render, and that matched loaders run in parallel.
Show judgment about cancellation through request.signal, where fetcher results land, and when a 4xx action result should skip revalidation.
Be ready to argue when route loaders are the right data boundary for a team versus a component-level cache, and what that does to ownership of data code.
## What a loader and an action are In **React Router 7's data mode** (a router created with `createBrowserRouter` and rendered through `<RouterProvider>`), data work is declared on the **route object** instead of inside components. Two route properties carry it: - **`loader`**: a function the router calls to get the data a route needs **before** that route's element renders. It runs on the initial load and on navigations that match the route, subject to the revalidation rules. - **`action`**: a function the router calls when a **non-GET submission** (POST, PUT, PATCH or DELETE) targets the route, typically from `<Form method="post">`, from `useSubmit`, or from a fetcher. Both may be `async`. Whatever value they return (a plain object, an array, `null`) becomes that route's loader data or action data. There is no special wrapper to call: in v7 you return plain values, and reach for the `data()` utility only when a status code or headers matter. ## The arguments they receive A loader and an action receive **one object**, the same shape for both: | Field | What it is | Typical use | |---|---|---| | `request` | a standard Fetch `Request` for this navigation or submission | `await request.formData()` in an action; pass `request.signal` to `fetch` | | `params` | the matched dynamic segments, e.g. `{ projectId: "42" }` | look up the record the URL names | | `url`, `pattern` | the request URL and the matched un-interpolated route pattern (stable since 7.15.0) | logging and instrumentation | Because `request` is a real Fetch `Request`, the router can **abort** it. When the user navigates away before a loader has finished, the router aborts that request's `signal`, so a loader that forwarded the signal to `fetch` cancels its stale HTTP call instead of racing the new page. ## How components read the results - **`useLoaderData()`** returns the value that the **component's own route** loader returned. It is available on the element's first render, because the router awaited the loader before rendering the element. - **`useActionData()`** returns what the action returned for the most recent **navigation** submission (a `<Form>` or `useSubmit`), and `undefined` before any such submission. The classic use is field-level validation errors rendered next to the inputs. - A submission made through a **fetcher** does not populate `useActionData`; its result lands on `fetcher.data` instead. ## What happens, in order On a navigation: 1. The user follows a link and the router matches the new URL against the route tree. 2. It calls the loaders of the matched routes that need data **in parallel**, not one after another. 3. When they settle, it commits the new location and renders the matched elements, which read their data with `useLoaderData`. On a submission: 1. `<Form method="post">` serialises its fields into `FormData` and calls the target route's action with a `Request` carrying that body. 2. When the action resolves with a successful status, the router **revalidates**: it calls the active loaders again so the page reflects the mutation. 3. The page re-renders with fresh loader data, and `useActionData` holds the action's return value. While either sequence runs, `useNavigation().state` reports `"loading"` or `"submitting"` instead of `"idle"`, so a pending indicator needs no local state. ## Contrast with fetching in useEffect, at the API level | Concern | `useEffect` + `useState` in the component | Route `loader` | |---|---|---| | When the request starts | after the component has rendered once | before the route element renders | | First render | a placeholder while data is `undefined` | data already in `useLoaderData` | | Pending state | a local `isLoading` flag per component | router-wide through `useNavigation` | | Cancellation | a hand-written `AbortController` in the cleanup | the router aborts `request.signal` | | Refresh after a write | a hand-written refetch call | automatic revalidation after an action | ## Setup and version notes - Loaders and actions work only in a **data router**. Calling `useLoaderData` under a plain `<BrowserRouter>` throws an error saying the hook must be used within a data router; the `loader` prop on a `<Route>` inside `<Routes>` is simply not run. - In v7 every API here is imported from `react-router`; the v6 package `react-router-dom` was collapsed into it. - v6's `json()` helper was **removed** in v7. Return plain objects, or `data(value, { status })` when the status matters, for example a 400 from an action with validation errors.
- In React Router 7, why does a component that reads useLoaderData need no loading branch for that data?Because the router calls the route's loader and waits for it before it commits the navigation and renders the element. By the time the component renders, the value is already in router state. The waiting is visible elsewhere: `useNavigation().state` is `"loading"` on the previous page while the next page's loaders run.
- In React Router 7, if two nested routes both have loaders, do they run one after the other?No. The router calls the loaders of all matched routes that need data in parallel and renders once they have settled. A child loader therefore cannot read its parent loader's result directly; if it needs the same record, it fetches it itself or both call a shared, cached function.
- In React Router 7, what is the difference between returning data({ error }, { status: 400 }) and throwing it from an action?Returning it makes the value action data: the page keeps rendering and `useActionData()` (or `fetcher.data`) holds the error, while the 4xx status stops the default loader revalidation. Throwing it treats it as a route error, so the nearest `errorElement` replaces the route's element.
A loader is like a waiter who brings the dishes before you are seated, so the table is set the moment you sit; an action is the order slip you hand back, after which the waiter refreshes what is on the table.
saying these in an interview costs you the question
- Says a loader runs inside the component after its first render, like an effect.
- Believes useActionData also receives results from fetcher submissions.
- Thinks useLoaderData works under a plain BrowserRouter without a data router.
- Claims matched nested loaders run sequentially, parent first.
- Wraps v7 loader returns in json(), which v7 removed.