Revalidation Optimization
On this page

Revalidation Optimization



After a mutation or some navigations, React Router re-runs loaders so the UI stays in sync with the server. That default is the right starting point. When a loader is expensive, or a mutation cannot affect that route's data, you can skip the reload.

Skipping revalidation can leave the UI out of sync with the server. Prefer targeting a specific action or navigation, and fall back to `defaultShouldRevalidate` instead of always returning `false`.

Default behavior

The default behavior differs between Framework and Data Modes:

  • Framework Mode with SSR
    • Defaults to opt-out behavior - active loaders are revalidated on navigations and successful submissions (Link, Form, fetcher.submit)
    • Failed submissions returning a 4xx/5xx status do not trigger revalidations by default
  • Framework "SPA Mode" and Data Mode
    • Defaults to opt-out behavior on successful submissions - active loaders are revalidated on successful submissions (Form, [fetcher.submit])
      • Failed submissions returning a 4xx/5xx status do not trigger revalidations by default
    • Defaults to opt-in behavior for GET navigations (Link) - active loaders are only revalidated if their dynamic params changed, or if any search params changed
      • A GET navigation to the exact same URL is treated like a page refresh and all loaders are revalidated.

Matched matched routes are handled independently - A child that skips revalidation does not skip any ancestor routes.

fetcher.load only revalidates by default after action submissions and explicit useRevalidator calls, not on search-param or param-driven navigations.

A plain fetch() to a resource route does not go through the router, so it does not revalidate loaders.

Skip a route with shouldRevalidate

Export shouldRevalidate from the route module (Framework Mode) or set it on the route object (Data Mode). Returning false skips that route's loader.

// Framework Mode
export function shouldRevalidate() {
  return false;
}
// Data Mode
createBrowserRouter([
  {
    path: "/dashboard",
    loader: dashboardLoader,
    shouldRevalidate: () => false,
    Component: Dashboard,
  },
]);

Always returning false opts that route out of the default behavior completely, including cases you usually still want (param changes, explicit useRevalidator). Prefer the conditional form below.

Opt out of specific requests

Inspect ShouldRevalidateFunctionArgs and return defaultShouldRevalidate for everything else.

import type { ShouldRevalidateFunctionArgs } from "react-router";

export function shouldRevalidate({
  formMethod,
  formAction,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  if (
    formMethod === "POST" &&
    formAction?.endsWith("/analytics")
  ) {
    return false;
  }

  return defaultShouldRevalidate;
}

Other useful fields:

  • formData, json, text — the submission body
  • actionResult, actionStatus — the action's return value
  • currentUrl, nextUrl, currentParams, nextParams — the navigation

You can ignore search-param-only updates while still revalidating when the pathname changes:

export function shouldRevalidate({
  currentUrl,
  nextUrl,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  if (currentUrl.pathname === nextUrl.pathname) {
    return false;
  }

  return defaultShouldRevalidate;
}

Skip revalidation for one event

Pass defaultShouldRevalidate={false} at the call site so you do not have to change every route file. This works on <Form>, <Link>, <fetcher.Form>, and as an option to useSubmit, fetcher.submit, useNavigate, and useSearchParams.

import { Form, Link } from "react-router";

<Link
  to="/search?q=shoes"
  defaultShouldRevalidate={false}
>
  Search Shoes
</Link>

<Form
  method="post"
  action="/analytics"
  defaultShouldRevalidate={false}
>
  <button>Track Click</button>
</Form>
fetcher.submit(
  { intent: "save-progress" },
  {
    method: "post",
    action: "/save-progress",
    defaultShouldRevalidate: false,
  },
);

If a matched route does not export shouldRevalidate, this value is used directly for that loader. If it does export shouldRevalidate, the value is passed in as defaultShouldRevalidate and the route still has the final say.

That is why a child shouldRevalidate that always returns false cannot hide a root reload after fetcher.submit. Either also opt root out for that case, or pass defaultShouldRevalidate: false at the call site when root has no shouldRevalidate of its own.

Docs and examples CC 4.0
Edit