diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts
index 0fdf16c762..84a1c2043c 100644
--- a/packages/react-router/__tests__/router/fetchers-test.ts
+++ b/packages/react-router/__tests__/router/fetchers-test.ts
@@ -2415,6 +2415,47 @@ describe("fetchers", () => {
});
});
+ it("skips all revalidation when callsite defaultShouldRevalidate is false", async () => {
+ let key = "key";
+ let actionKey = "actionKey";
+ let t = setup({
+ routes: TASK_ROUTES,
+ initialEntries: ["/"],
+ hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
+ });
+
+ // preload a fetcher
+ let A = await t.fetch("/tasks/1", key);
+ await A.loaders.tasksId.resolve("TASKS ID");
+ expect(t.fetchers[key]).toMatchObject({
+ state: "idle",
+ data: "TASKS ID",
+ });
+
+ // submit action with shouldRevalidate=false
+ let C = await t.fetch("/tasks", actionKey, {
+ formMethod: "post",
+ formData: createFormData({}),
+ defaultShouldRevalidate: false,
+ });
+
+ expect(t.fetchers[actionKey]).toMatchObject({ state: "submitting" });
+
+ // resolve action — no loaders should trigger
+ await C.actions.tasks.resolve("TASKS ACTION");
+
+ // verify all fetchers idle
+ expect(t.fetchers[key]).toMatchObject({
+ state: "idle",
+ data: "TASKS ID",
+ });
+
+ expect(t.fetchers[actionKey]).toMatchObject({
+ state: "idle",
+ data: "TASKS ACTION",
+ });
+ });
+
it("does not revalidate fetchers initiated from removed routes", async () => {
let t = setup({
routes: TASK_ROUTES,
diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts
index fa00d6c6a2..c9d1a98e18 100644
--- a/packages/react-router/lib/context.ts
+++ b/packages/react-router/lib/context.ts
@@ -154,6 +154,8 @@ export interface NavigateOptions {
flushSync?: boolean;
/** Enables a {@link https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API View Transition} for this navigation by wrapping the final state update in `document.startViewTransition()`. If you need to apply specific styles for this view transition, you will also need to leverage the {@link https://api.reactrouter.com/v7/functions/react_router.useViewTransitionState.html useViewTransitionState()} hook. */
viewTransition?: boolean;
+ /** When false, suppresses loader revalidation triggered by this navigation **/
+ defaultShouldRevalidate?: boolean;
}
/**
diff --git a/packages/react-router/lib/dom/dom.ts b/packages/react-router/lib/dom/dom.ts
index 82404939fb..c01c9d9143 100644
--- a/packages/react-router/lib/dom/dom.ts
+++ b/packages/react-router/lib/dom/dom.ts
@@ -192,6 +192,11 @@ interface SharedSubmitOptions {
* Enable flushSync for this submission's state updates
*/
flushSync?: boolean;
+
+ /**
+ * Determine if revalidation should occur post-submission.
+ */
+ defaultShouldRevalidate?: boolean;
}
/**
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index cff63d829d..22c1eed9cb 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -1287,6 +1287,22 @@ export interface LinkProps
* To apply specific styles for the transition, see {@link useViewTransitionState}
*/
viewTransition?: boolean;
+
+ /**
+ * Controls whether loaders should revalidate when this link is clicked.
+ *
+ * ```tsx
+ *
+ * ```
+ *
+ * When set to `false`, prevents the default revalidation behavior after navigation,
+ * keeping the current loader data without refetching. This can be useful when updating
+ * search params and you don't want to trigger a revalidation.
+ *
+ * By default (when not specified), loaders will revalidate according to the framework's
+ * standard revalidation behavior.
+ */
+ defaultShouldRevalidate?: boolean;
}
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
@@ -1319,6 +1335,7 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
* @param {LinkProps.state} props.state n/a
* @param {LinkProps.to} props.to n/a
* @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a
+ * @param {LinkProps.defaultShouldRevalidate} props.defaultShouldRevalidate n/a
*/
export const Link = React.forwardRef(
function LinkWithRef(
@@ -1334,6 +1351,7 @@ export const Link = React.forwardRef(
to,
preventScrollReset,
viewTransition,
+ defaultShouldRevalidate,
...rest
},
forwardedRef,
@@ -1389,6 +1407,7 @@ export const Link = React.forwardRef(
preventScrollReset,
relative,
viewTransition,
+ defaultShouldRevalidate,
});
function handleClick(
event: React.MouseEvent,
@@ -1788,6 +1807,11 @@ interface SharedFormProps extends React.FormHTMLAttributes {
* then this form will not do anything.
*/
onSubmit?: React.FormEventHandler;
+
+ /**
+ * Determine if revalidation should occur post-submission.
+ */
+ defaultShouldRevalidate?: boolean;
}
/**
@@ -1911,6 +1935,7 @@ type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement;
* @param {FormProps.replace} replace n/a
* @param {FormProps.state} state n/a
* @param {FormProps.viewTransition} viewTransition n/a
+ * @param {FormProps.defaultShouldRevalidate} defaultShouldRevalidate n/a
* @returns A progressively enhanced [`