skip to content

React Router

React Router is the default routing library for React single-page apps: nested routes, dynamic segments, loaders and actions, and hooks for navigation. Expect questions on how you configure routes and how you keep the protected ones protected.

on this pageshow

explore

questions

28

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?

level: juniorimportance: must knowfreq 72%

answer

  1. declared on the route object
  2. a Fetch Request plus matched params
  3. before render versus on submission
  4. one hook named after each

basics

~20 s

In 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 s

In 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 lines
ts
import { 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

for a junior

Recall the pair: loader for reading before render, action for non-GET submissions, and the two hooks useLoaderData and useActionData that read them.

for a middle

Explain the arguments, request and params, why the loader data exists on first render, and that matched loaders run in parallel.

for a senior

Show judgment about cancellation through request.signal, where fetcher results land, and when a 4xx action result should skip revalidation.

for a principal

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.
open as a page

In React Router v7, how do you configure /projects/:projectId/tasks/:taskId so every screen renders inside one shared dashboard shell?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Nest the routes: a shell route whose element renders the sidebar and an <Outlet />, with projects/:projectId and then tasks/:taskId as descendants in children. Child paths are relative and each parent renders its matched child where it places <Outlet />.

open as a page

In React Router v7, what type are the values useParams returns, and what must you do before using :productId as a number?

level: juniorimportance: must knowfreq 64%

basics

~20 s

useParams values are strings or undefined, decoded from the URL and never coerced. A :productId segment matches any text, so parse it, check the result, and render a not-found or error state when it is missing or invalid.

open as a page

In React Router v7, what is the difference between rendering <BrowserRouter> with <Routes> and creating a router with createBrowserRouter and <RouterProvider>?

level: juniorimportance: must knowfreq 72%

basics

~20 s

<BrowserRouter> with <Routes> is declarative mode: routes are matched while React renders, giving URL matching and navigation only. createBrowserRouter builds a data router outside React, which alone runs loaders and actions, tracks pending navigations and renders route error boundaries.

open as a page

In React Router, how do you write a RequireAuth wrapper that sends signed-out users to /login while remembering the page they asked for?

level: juniorimportance: must knowfreq 72%

basics

~10 s

A RequireAuth component reads the auth state and useLocation(); when signed out it returns <Navigate to="/login" replace state={{ from: location }} />, otherwise its children or <Outlet />. The login page later reads location.state.from.

open as a page

In React Router 7, why build an inline like button with useFetcher instead of <Form>, and how do you show its pending and optimistic state?

level: middleimportance: must knowfreq 58%

basics

~20 s

In React Router 7, a <Form> submission is a navigation to the action's URL and drives page-wide useNavigation state; useFetcher calls the same action without navigating. fetcher.state gives per-button pending UI, and fetcher.formData lets the button render the new value optimistically.

open as a page

In React Router v7, how does a relative to like ".." resolve inside nested routes, and what does relative="path" change?

level: middleimportance: must knowfreq 57%

basics

~20 s

Relative to values resolve against the route hierarchy where the link is rendered, so ".." moves up one route and drops that route's whole path pattern. relative="path" makes ".." drop a single URL segment instead.

open as a page

In React Router v7, sibling routes "projects/new" and "projects/:projectId" both match /projects/new; which renders, and does declaration order matter?

level: middleimportance: must knowfreq 52%

basics

~20 s

projects/new renders, whatever the order. React Router ranks every candidate route by specificity, so a static segment outranks a dynamic one and a splat ranks lowest. Declaration order only decides between siblings with identical scores.

open as a page

In React Router v7, why does setSearchParams({ page: "2" }) drop ?category and ?sort, and how do you change one param while keeping the rest?

level: middleimportance: must knowfreq 62%

basics

~20 s

setSearchParams replaces the entire query string with what you pass, so an object with only page discards everything else. Use the callback form: take the copy it receives, set or delete the one key, and return it.

open as a page

In a React Router data router, what happens step by step when a route's loader throws redirect('/login?redirectTo=/admin/users')?

level: middleimportance: must knowfreq 62%

basics

~20 s

redirect() builds a 302 Response with a Location header. When a loader throws it, the router lets the navigation's loaders settle, then starts a new navigation to /login; the protected page never renders and its URL is never committed to history.

open as a page

In React Router, what does marking a route with index: true do, and at which URL does that route render?

level: juniorimportance: should knowfreq 55%

basics

~20 s

An index route (index: true, no path) is a parent's default child: it renders in the parent's <Outlet> when the URL matches the parent's path exactly, such as /dashboard. Index routes cannot have children of their own.

open as a page

In React Router 7's data mode, how do you render a post immediately while its slow comments section streams in, now that defer is gone?

level: middleimportance: should knowfreq 45%

basics

~20 s

In React Router 7, the loader awaits only the post and returns the unawaited comments promise as a property of its result; the component renders <Await resolve={comments}> inside a <Suspense> fallback, so the post shows at once and comments fill in later.

open as a page

In React Router 7's data router, what happens when a loader throws, and how does the error element tell a deliberate 404 from a crash?

level: middleimportance: should knowfreq 52%

basics

~20 s

In a React Router 7 data router, a thrown loader error renders the nearest route errorElement or ErrorBoundary in place of that route's element. The boundary reads it with useRouteError; isRouteErrorResponse is true for a thrown data(..., { status: 404 }).

open as a page

In React Router v7, when do you call useNavigate's navigate() and when do you render <Navigate>, and why must navigate() not run during render?

level: middleimportance: should knowfreq 52%

basics

~20 s

Call navigate() from event handlers or effects after something happens, such as a save completing. Render <Navigate> when the render output itself should be a redirect. navigate() during a component's first render only logs a warning and is ignored.

open as a page

In React Router v7, which URLs do the route paths ":lang?/categories" and "files/*" match, and where may the * appear?

level: middleimportance: should knowfreq 44%

basics

~10 s

:lang?/categories matches /categories and /en/categories, with lang undefined when absent. files/* matches /files and anything below it, like /files/a/b. A * only works as the final segment, directly after a slash.

open as a page

In React Router v7, how does a project layout route share its project with child routes through <Outlet context>, and how far does that value reach?

level: middleimportance: should knowfreq 38%

basics

~20 s

The layout renders <Outlet context={value} /> and child route components call useOutletContext() to read it. The value reaches the route rendered in that outlet and its components, but not grandchild routes behind another bare <Outlet />.

open as a page

In a React Router v7 data router, what does a route object's lazy property load, and which route properties can it never supply?

level: middleimportance: should knowfreq 38%

basics

~20 s

route.lazy loads a matched route's non-matching properties — Component, loader, action, ErrorBoundary and similar — on first navigation to it. It can never supply path, index, children, id or caseSensitive, because the router needs those to match before loading anything.

open as a page

In React Router, when both /teams/new and /teams/:teamId are defined, which route renders for /teams/new, and how does the router decide?

level: middleimportance: should knowfreq 48%

basics

~20 s

/teams/new renders the static route. React Router scores every candidate branch — static segments weigh most, dynamic segments less, a splat is penalised — and picks the highest score, so declaration order only breaks ties between siblings.

open as a page

In React Router v7, how does a login page send the user back to location.state.from or a redirectTo param, and what must it validate?

level: middleimportance: should knowfreq 55%

basics

~20 s

After sign-in, a component calls navigate(from, { replace: true }); a data-router login action returns redirect(target). A redirectTo read from the URL is attacker-controllable, so accept only a same-app path starting with a single '/' and fall back to '/'.

open as a page

A React Router 7 dashboard autosaves a draft through a fetcher every few seconds, and each save reruns every loader, including a slow reports loader. Why, and how do you stop it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

React Router 7 reruns every active loader after any action that returns 2xx, fetcher submissions included, because it cannot know what the mutation changed. Opt the reports route out with shouldRevalidate keyed on formAction, or pass defaultShouldRevalidate false on the autosave submission.

open as a page

In a React Router v7 settings area, a Back button and the post-save redirect both call navigate(-1); what breaks for deep-linked users, and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

navigate(-1) steps back in the browser history, which is outside the app when the user opened the edit URL directly, so they are sent away. Go back only when location.key is not "default"; otherwise navigate to a known parent route with replace.

open as a page

In a React Router v7 dashboard, switching from /projects/1/tasks/7 to /projects/1/tasks/8 leaves the task form showing task 7's draft; why, and how do you fix it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Both URLs match the same tasks/:taskId route, so React Router renders the same element in the same place and React keeps the component instance and its state. Key the form by the task id, or make effects depend on it.

open as a page

In a React Router v7 product list, a category handler calls setSearchParams twice, once for category and once to reset page, and the category change is lost; why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The setSearchParams callback does not queue like React's setState: both calls start from the params of the current render, and each starts a navigation. The second call's URL, which lacks the new category, wins. Make every change in one call.

open as a page

You are moving a large React Router app from <BrowserRouter> with descendant <Routes> to createBrowserRouter; how do you migrate incrementally, and what silently breaks along the way?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Wrap the existing app in a data router with a single catch-all route (path '*') that renders the old <Routes>, then lift routes into route objects one at a time. Loaders placed on still-descendant routes silently never run.

open as a page

A React Router data router guards /admin with a parent loader that throws redirect, yet signed-out visits still hit the admin API from a child loader; why, and what fixes it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The data router runs all matched loaders in parallel and acts on a redirect only after they settle, so the child loader fetches anyway. Guard with route middleware (stable since 7.9.0) or a check in every loader, and authorize on the server.

open as a page