Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt

Use this file to discover all available pages before exploring further.

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