Skip to main content

shouldRevalidate

Optimizes data loading by allowing you to skip loader execution when it’s not necessary.

Signature

export function shouldRevalidate(args: ShouldRevalidateFunctionArgs): boolean
args
ShouldRevalidateFunctionArgs
required
Arguments passed to the shouldRevalidate function
return
boolean
  • true to run the loader
  • false to skip the loader and keep existing data

Default Behavior

By default, React Router revalidates (re-runs loaders) in these cases:
  • After an action is called (form submission)
  • When URL search params change
  • When a link is clicked to the same URL (user clicks “refresh”)
  • When params change for the route
React Router does NOT revalidate when:
  • Navigating to a different route with the same parent
  • Params haven’t changed
  • No action was called

Basic Example

export async function loader({ params }: Route.LoaderArgs) {
  return { products: await fetchProducts(params.category) };
}

export function shouldRevalidate({ currentParams, nextParams }: ShouldRevalidateFunctionArgs) {
  // Only revalidate if category param changed
  return currentParams.category !== nextParams.category;
}

Skip Revalidation on Specific Actions

export function shouldRevalidate({
  formAction,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Don't revalidate when updating user preferences
  if (formAction === "/api/preferences") {
    return false;
  }
  
  // Use default behavior for everything else
  return defaultShouldRevalidate;
}

Revalidate Based on Action Result

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const success = await updateProfile(formData);
  
  // Include a flag in the result
  return { success, shouldRevalidate: success };
}

export function shouldRevalidate({
  actionResult,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Only revalidate if action was successful
  if (actionResult && "shouldRevalidate" in actionResult) {
    return actionResult.shouldRevalidate;
  }
  
  return defaultShouldRevalidate;
}

Skip Revalidation on Search Param Changes

export function shouldRevalidate({
  currentUrl,
  nextUrl,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Only revalidate if pathname changed, not search params
  if (currentUrl.pathname === nextUrl.pathname) {
    return false;
  }
  
  return defaultShouldRevalidate;
}

export default function SearchResults() {
  const [searchParams] = useSearchParams();
  const data = useLoaderData<typeof loader>();
  
  // Filter client-side based on search params
  const filtered = data.products.filter((p) =>
    p.name.includes(searchParams.get("q") || "")
  );
  
  return <ProductList products={filtered} />;
}

Revalidate Only on Specific Param Changes

export function shouldRevalidate({
  currentParams,
  nextParams,
  currentUrl,
  nextUrl,
}: ShouldRevalidateFunctionArgs) {
  // Params we care about
  const significantParams = ["category", "brand"];
  
  // Check if any significant param changed
  const paramsChanged = significantParams.some(
    (param) => currentParams[param] !== nextParams[param]
  );
  
  // Or if the pathname changed
  const pathnameChanged = currentUrl.pathname !== nextUrl.pathname;
  
  return paramsChanged || pathnameChanged;
}

Time-Based Revalidation

let lastFetch = 0;
const CACHE_TIME = 60_000; // 1 minute

export async function loader() {
  lastFetch = Date.now();
  return { data: await fetchData() };
}

export function shouldRevalidate({
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Revalidate if cache is stale
  const isStale = Date.now() - lastFetch > CACHE_TIME;
  
  if (isStale) {
    return true;
  }
  
  return defaultShouldRevalidate;
}

Revalidate on Specific Form Methods

export function shouldRevalidate({
  formMethod,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Always revalidate on POST/PUT/DELETE
  if (formMethod && ["POST", "PUT", "DELETE"].includes(formMethod)) {
    return true;
  }
  
  // Skip revalidation on GET (search forms)
  if (formMethod === "GET") {
    return false;
  }
  
  return defaultShouldRevalidate;
}

Inspect Action Status

export function shouldRevalidate({
  actionStatus,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Don't revalidate on client errors (4xx)
  if (actionStatus && actionStatus >= 400 && actionStatus < 500) {
    return false;
  }
  
  return defaultShouldRevalidate;
}

Complex Example

export function shouldRevalidate({
  currentParams,
  nextParams,
  currentUrl,
  nextUrl,
  formAction,
  formMethod,
  actionResult,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Always revalidate after mutations
  if (formMethod && formMethod !== "GET") {
    return true;
  }
  
  // Don't revalidate if action explicitly says not to
  if (actionResult?.skipRevalidation) {
    return false;
  }
  
  // Only revalidate if the product ID changed
  if (currentParams.productId !== nextParams.productId) {
    return true;
  }
  
  // Don't revalidate on tab/section changes (query params)
  if (
    currentUrl.pathname === nextUrl.pathname &&
    currentParams.productId === nextParams.productId
  ) {
    return false;
  }
  
  return defaultShouldRevalidate;
}

Best Practices

Use the default behavior as a fallback to avoid missing important revalidations:
export function shouldRevalidate({
  currentParams,
  nextParams,
  defaultShouldRevalidate,
}: ShouldRevalidateFunctionArgs) {
  // Your custom logic
  if (currentParams.id === nextParams.id) {
    return false;
  }
  
  // ✅ Fall back to default
  return defaultShouldRevalidate;
}
It’s better to revalidate unnecessarily than to show stale data:
// ❌ Too aggressive - might show stale data
export function shouldRevalidate() {
  return false; // Never revalidate
}

// ✅ Conservative - only skip when safe
export function shouldRevalidate({
  currentParams,
  nextParams,
  defaultShouldRevalidate,
}) {
  // Only skip when we're certain data hasn't changed
  if (currentParams.id === nextParams.id && !formMethod) {
    return false;
  }
  return defaultShouldRevalidate;
}
Only add shouldRevalidate when loader is computationally expensive:
// Worth optimizing - expensive query
export async function loader({ params }: Route.LoaderArgs) {
  return {
    analytics: await runComplexAnalytics(params.reportId),
  };
}

export function shouldRevalidate({ currentParams, nextParams }) {
  return currentParams.reportId !== nextParams.reportId;
}
Ensure your revalidation logic doesn’t break data updates:
// Test cases to verify:
// 1. Navigation between different items
// 2. Form submissions
// 3. Search param changes
// 4. Same URL clicks (refresh)
// 5. Action success/failure

Common Patterns

Parent/Child Route Coordination

// Parent doesn't need to reload when child params change
export function shouldRevalidate({
  currentParams,
  nextParams,
}: ShouldRevalidateFunctionArgs) {
  // Only revalidate if parent params changed
  return currentParams.parentId !== nextParams.parentId;
}

List/Detail Pattern

// List route - don't revalidate when viewing details
export function shouldRevalidate({
  currentUrl,
  nextUrl,
  formMethod,
}: ShouldRevalidateFunctionArgs) {
  // Only revalidate if list was mutated
  if (formMethod && formMethod !== "GET") {
    return true;
  }
  
  // Don't revalidate when navigating to/from detail
  return false;
}

See Also

Build docs developers (and LLMs) love