Skip to content

Commit 3ee7f30

Browse files
committed
Add callsite revalidation optout
1 parent 13abd80 commit 3ee7f30

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

packages/react-router/__tests__/router/fetchers-test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2415,6 +2415,47 @@ describe("fetchers", () => {
24152415
});
24162416
});
24172417

2418+
it("skips all revalidation when shouldRevalidate is false", async () => {
2419+
let key = "key";
2420+
let actionKey = "actionKey";
2421+
let t = setup({
2422+
routes: TASK_ROUTES,
2423+
initialEntries: ["/"],
2424+
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
2425+
});
2426+
2427+
// preload a fetcher
2428+
let A = await t.fetch("/tasks/1", key);
2429+
await A.loaders.tasksId.resolve("TASKS ID");
2430+
expect(t.fetchers[key]).toMatchObject({
2431+
state: "idle",
2432+
data: "TASKS ID",
2433+
});
2434+
2435+
// submit action with shouldRevalidate=false
2436+
let C = await t.fetch("/tasks", actionKey, {
2437+
formMethod: "post",
2438+
formData: createFormData({}),
2439+
shouldRevalidate: false,
2440+
});
2441+
2442+
expect(t.fetchers[actionKey]).toMatchObject({ state: "submitting" });
2443+
2444+
// resolve action — no loaders should trigger
2445+
await C.actions.tasks.resolve("TASKS ACTION");
2446+
2447+
// verify all fetchers idle
2448+
expect(t.fetchers[key]).toMatchObject({
2449+
state: "idle",
2450+
data: "TASKS ID",
2451+
});
2452+
2453+
expect(t.fetchers[actionKey]).toMatchObject({
2454+
state: "idle",
2455+
data: "TASKS ACTION",
2456+
});
2457+
});
2458+
24182459
it("does not revalidate fetchers initiated from removed routes", async () => {
24192460
let t = setup({
24202461
routes: TASK_ROUTES,

packages/react-router/lib/dom/dom.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ interface SharedSubmitOptions {
192192
* Enable flushSync for this submission's state updates
193193
*/
194194
flushSync?: boolean;
195+
196+
/**
197+
* Determines whether revalidation should occur in certain conditions.
198+
*/
199+
shouldRevalidate?: boolean | (() => boolean);
195200
}
196201

197202
/**

packages/react-router/lib/dom/lib.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,11 @@ interface SharedFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
17881788
* then this form will not do anything.
17891789
*/
17901790
onSubmit?: React.FormEventHandler<HTMLFormElement>;
1791+
1792+
/**
1793+
* Determine if revalidation should occur post-submission.
1794+
*/
1795+
shouldRevalidate?: boolean | (() => boolean);
17911796
}
17921797

17931798
/**
@@ -1911,6 +1916,7 @@ type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement;
19111916
* @param {FormProps.replace} replace n/a
19121917
* @param {FormProps.state} state n/a
19131918
* @param {FormProps.viewTransition} viewTransition n/a
1919+
* @param {FormProps.shouldRevalidate} shouldRevalidate n/a
19141920
* @returns A progressively enhanced [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) component
19151921
*/
19161922
export const Form = React.forwardRef<HTMLFormElement, FormProps>(
@@ -1928,6 +1934,7 @@ export const Form = React.forwardRef<HTMLFormElement, FormProps>(
19281934
relative,
19291935
preventScrollReset,
19301936
viewTransition,
1937+
shouldRevalidate,
19311938
...props
19321939
},
19331940
forwardedRef,
@@ -1960,6 +1967,7 @@ export const Form = React.forwardRef<HTMLFormElement, FormProps>(
19601967
relative,
19611968
preventScrollReset,
19621969
viewTransition,
1970+
shouldRevalidate,
19631971
});
19641972
};
19651973

@@ -2549,6 +2557,7 @@ export function useSubmit(): SubmitFunction {
25492557
if (options.navigate === false) {
25502558
let key = options.fetcherKey || getUniqueFetcherId();
25512559
await router.fetch(key, currentRouteId, options.action || action, {
2560+
shouldRevalidate: options.shouldRevalidate,
25522561
preventScrollReset: options.preventScrollReset,
25532562
formData,
25542563
body,
@@ -2558,6 +2567,7 @@ export function useSubmit(): SubmitFunction {
25582567
});
25592568
} else {
25602569
await router.navigate(options.action || action, {
2570+
shouldRevalidate: options.shouldRevalidate,
25612571
preventScrollReset: options.preventScrollReset,
25622572
formData,
25632573
body,

packages/react-router/lib/router/router.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ type BaseNavigateOrFetchOptions = {
524524
preventScrollReset?: boolean;
525525
relative?: RelativeRoutingType;
526526
flushSync?: boolean;
527+
shouldRevalidate?: boolean | (() => boolean);
527528
};
528529

529530
// Only allowed for navigations
@@ -1548,6 +1549,14 @@ export function createRouter(init: RouterInit): Router {
15481549
return;
15491550
}
15501551

1552+
let shouldRevalidate =
1553+
opts && "shouldRevalidate" in opts
1554+
? typeof opts.shouldRevalidate === "function"
1555+
? opts.shouldRevalidate()
1556+
: // undefined should eval to true
1557+
opts.shouldRevalidate !== false
1558+
: true;
1559+
15511560
await startNavigation(historyAction, nextLocation, {
15521561
submission,
15531562
// Send through the formData serialization error if we have one so we can
@@ -1556,6 +1565,7 @@ export function createRouter(init: RouterInit): Router {
15561565
preventScrollReset,
15571566
replace: opts && opts.replace,
15581567
enableViewTransition: opts && opts.viewTransition,
1568+
shouldRevalidate,
15591569
flushSync,
15601570
});
15611571
}
@@ -1632,6 +1642,7 @@ export function createRouter(init: RouterInit): Router {
16321642
replace?: boolean;
16331643
enableViewTransition?: boolean;
16341644
flushSync?: boolean;
1645+
shouldRevalidate?: boolean;
16351646
},
16361647
): Promise<void> {
16371648
// Abort any in-progress navigations and start a new one. Unset any ongoing
@@ -1771,6 +1782,15 @@ export function createRouter(init: RouterInit): Router {
17711782

17721783
matches = actionResult.matches || matches;
17731784
pendingActionResult = actionResult.pendingActionResult;
1785+
1786+
if (opts.shouldRevalidate === false) {
1787+
completeNavigation(location, {
1788+
matches,
1789+
...getActionDataForCommit(pendingActionResult),
1790+
});
1791+
return;
1792+
}
1793+
17741794
loadingNavigation = getLoadingNavigation(location, opts.submission);
17751795
flushSync = false;
17761796
// No need to do fog of war matching again on loader execution
@@ -2346,6 +2366,13 @@ export function createRouter(init: RouterInit): Router {
23462366
let preventScrollReset = (opts && opts.preventScrollReset) === true;
23472367

23482368
if (submission && isMutationMethod(submission.formMethod)) {
2369+
let shouldRevalidate =
2370+
opts && "shouldRevalidate" in opts
2371+
? typeof opts.shouldRevalidate === "function"
2372+
? opts.shouldRevalidate()
2373+
: // undefined should eval to true
2374+
opts.shouldRevalidate !== false
2375+
: true;
23492376
await handleFetcherAction(
23502377
key,
23512378
routeId,
@@ -2356,6 +2383,7 @@ export function createRouter(init: RouterInit): Router {
23562383
flushSync,
23572384
preventScrollReset,
23582385
submission,
2386+
shouldRevalidate,
23592387
);
23602388
return;
23612389
}
@@ -2388,6 +2416,7 @@ export function createRouter(init: RouterInit): Router {
23882416
flushSync: boolean,
23892417
preventScrollReset: boolean,
23902418
submission: Submission,
2419+
shouldRevalidate: boolean,
23912420
) {
23922421
interruptActiveLoads();
23932422
fetchLoadMatches.delete(key);
@@ -2563,6 +2592,7 @@ export function createRouter(init: RouterInit): Router {
25632592
basename,
25642593
init.patchRoutesOnNavigation != null,
25652594
[match.route.id, actionResult],
2595+
shouldRevalidate,
25662596
);
25672597

25682598
// Put all revalidating fetchers into the loading state, except for the
@@ -2594,6 +2624,15 @@ export function createRouter(init: RouterInit): Router {
25942624
abortPendingFetchRevalidations,
25952625
);
25962626

2627+
if (!shouldRevalidate) {
2628+
if (state.fetchers.has(key)) {
2629+
let doneFetcher = getDoneFetcher(actionResult.data);
2630+
state.fetchers.set(key, doneFetcher);
2631+
}
2632+
fetchControllers.delete(key);
2633+
return;
2634+
}
2635+
25972636
let { loaderResults, fetcherResults } =
25982637
await callLoadersAndMaybeResolveData(
25992638
dsMatches,
@@ -4820,6 +4859,7 @@ function getMatchesToLoad(
48204859
basename: string | undefined,
48214860
hasPatchRoutesOnNavigation: boolean,
48224861
pendingActionResult?: PendingActionResult,
4862+
shouldRevalidate?: boolean,
48234863
): {
48244864
dsMatches: DataStrategyMatch[];
48254865
revalidatingFetchers: RevalidatingFetcher[];
@@ -4855,7 +4895,8 @@ function getMatchesToLoad(
48554895
let actionStatus = pendingActionResult
48564896
? pendingActionResult[1].statusCode
48574897
: undefined;
4858-
let shouldSkipRevalidation = actionStatus && actionStatus >= 400;
4898+
let shouldSkipRevalidation =
4899+
(actionStatus && actionStatus >= 400) || shouldRevalidate === false;
48594900

48604901
let baseShouldRevalidateArgs = {
48614902
currentUrl,

0 commit comments

Comments
 (0)