skip to content

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%

answer

  1. the router can't see what changed
  2. fetcher submissions count too
  3. a per-route opt-out function
  4. a call-site default since 7.15

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.

solid answer

~40 s

After an action completes with a 2xx result, React Router 7 revalidates every active loader on the page; the router has no way to know what the action changed, and a `fetcher.submit` counts just like a `<Form>` here. An autosave every few seconds therefore reloads the slow reports loader each time. I'd fix it in the narrowest place. On the reports route I add `shouldRevalidate`, return `false` when `formAction` points at the drafts endpoint, and otherwise return `defaultShouldRevalidate`, because defining the function replaces the default logic entirely. Since 7.15.0 I can also pass `defaultShouldRevalidate: false` on the autosave `fetcher.submit`, which routes without their own `shouldRevalidate` obey directly. Either way I accept some staleness, so I keep a deliberate refresh path such as `useRevalidator()` on window focus.

code

ts · 15 lines
ts
import type { ShouldRevalidateFunctionArgs } from "react-router";
import { getReports } from "./api";
import { ReportsPanel } from "./ReportsPanel";

export const reportsRoute = {
  path: "reports",
  Component: ReportsPanel,
  loader: async () => ({ reports: await getReports() }),
  shouldRevalidate: ({ formAction, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) => {
    if (formAction?.startsWith("/drafts/")) {
      return false; // draft saves never change reports
    }
    return defaultShouldRevalidate;
  },
};

go deeper

for a junior

Recall that React Router reloads the page's loaders after an action by default, and that a fetcher submission is still an action call.

for a middle

Explain the default revalidation rules, including the 2xx-only rule for actions, and what shouldRevalidate receives and returns.

for a senior

Diagnose the storm, pick the narrowest fix between route shouldRevalidate and the call-site default, and name the staleness you are accepting.

for a principal

Decide where freshness policy should live for a large app, per route or per mutation, and when a dedicated cache in front of loaders is the better answer.

## Why every loader reruns After an action, a **React Router 7** data router cannot know which data the mutation touched: the action is an arbitrary function. So by default it **revalidates every active loader** once the action completes successfully, whether the action was submitted by `<Form>`, `useSubmit`, `fetcher.Form` or `fetcher.submit`. That is the "UI always matches the server" guarantee, and for an occasional save it is the right default. For an **autosave fetcher that posts every few seconds**, it means the slow reports loader, the root user loader and every other loader on the page rerun on each save. The default rules the router applies to a loader that is already on screen: - **after an action that returns a 2xx result**: rerun; - **after an action that returns or throws a 4xx/5xx response**: do not rerun by default (this became the default in v7; in v6 it was the opt-in future flag `v7_skipActionErrorRevalidation`); - on a navigation, **when the route's params change or the URL's search string changes**: rerun; - on a navigation to the **same URL** (like a hard reload): rerun; - when `useRevalidator().revalidate()` is called: rerun. Loaders attached to fetchers through `fetcher.load` also revalidate after actions. A **new route instance** (a route that was not on screen before) always runs its loader; nothing can skip that first load. ## Diagnosing it 1. Log in the reports loader, or watch the network panel, and confirm that one request fires per autosave. 2. Check that the fetcher's action is returning a **2xx**, so the default rule applies. 3. Confirm that no loader has a `shouldRevalidate` yet; if one exists, it may already be returning `true` unconditionally. ## Fix 1: `shouldRevalidate` on the expensive route A route object can define `shouldRevalidate(args)`. The router calls it whenever it is about to rerun that route's loader for a navigation or after an action, and passes what triggered the decision: `currentUrl`, `nextUrl`, `currentParams`, `nextParams`, the submission's `formMethod`, `formAction` and `formData`, the `actionResult`, the `actionStatus`, and **`defaultShouldRevalidate`**, which is what the router would have done on its own. ```ts shouldRevalidate: ({ formAction, defaultShouldRevalidate }) => formAction?.startsWith("/drafts/") ? false : defaultShouldRevalidate, ``` Defining the function **replaces the default logic completely** for that route. Returning a bare `false` would also freeze the reports on search-param and param changes, so the safe pattern is: decide the case you care about, and **fall through to `defaultShouldRevalidate`** for everything else. ## Fix 2: `defaultShouldRevalidate` at the call site Since **React Router 7.15.0**, the submission itself can carry a default: `defaultShouldRevalidate` on `<Form>`, `fetcher.Form` and `<Link>`, and as an option to `useSubmit` and `fetcher.submit`, for example `fetcher.submit(formData, { method: "post", defaultShouldRevalidate: false })`. - For routes **without** a `shouldRevalidate`, the value is used directly, so no loader reruns after that submission. - For routes **with** a `shouldRevalidate`, the value is passed in as its `defaultShouldRevalidate` argument, and the route keeps the final say. This fits autosave well: the draft editor already holds the text, so nothing on the page needs to reload after a save. ## Fix 3: let the action say what changed The action can return a small result, such as `{ touched: ["draft"] }`, and a route's `shouldRevalidate` can read `actionResult` to decide whether its data could have changed. Do **not** return an error status from a successful action just to suppress revalidation: a 4xx means something failed, and error handling, logging and any caller reading `actionStatus` will treat it that way. ## Trade-offs | Technique | Scope | Risk | |---|---|---| | route `shouldRevalidate` keyed on `formAction` | one route, all submitters | a new mutation that does affect reports is silently skipped | | call-site `defaultShouldRevalidate: false` | one submission | another route that should refresh stays stale | | `actionResult`-driven decision | one route, per result | couples loaders to action result shapes | | debouncing the autosave | request volume | saves arrive later; still revalidates each time | Skipping revalidation trades network work for **staleness**. The senior move is to skip only where you can say why the data cannot have changed, and to keep a `useRevalidator().revalidate()` path (on window focus, say) for the rest.

  • In React Router 7, why is shouldRevalidate: () => false on the reports route a bad fix?
    Defining `shouldRevalidate` replaces the default logic entirely, and the function is consulted on navigations as well as after actions. A constant `false` stops the reports loader rerunning when its params or search string change, so filters appear to do nothing. Decide the specific case and return `defaultShouldRevalidate` for the rest.
  • In React Router 7, a validation failure in the autosave action returns data({ errors }, { status: 422 }). Do loaders rerun?
    Not by default. In v7 an action result with a 4xx or 5xx status does not trigger revalidation, because such a result usually means nothing changed; `defaultShouldRevalidate` is `false` in that case. A route that still wants to reload can check `actionStatus` or `actionResult` in its `shouldRevalidate`.
  • In React Router 7, can shouldRevalidate stop a route's loader from running when the user first navigates to that route?
    No. It only governs routes that are already on screen. When a route instance is new to the page, its loader always runs, because there is no data to keep.

saying these in an interview costs you the question

  • Believes fetcher submissions do not trigger loader revalidation.
  • Returns a constant false from shouldRevalidate and breaks search-param reloads.
  • Thinks shouldRevalidate can skip a route's first load.
  • Assumes only the action's own route reloads after a submission.
  • Returns fake 4xx statuses from successful actions just to stop revalidation.