Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/react-router/__tests__/router/fetchers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,47 @@ describe("fetchers", () => {
});
});

it("skips all revalidation when shouldRevalidate 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({}),
shouldRevalidate: 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,
Expand Down
5 changes: 5 additions & 0 deletions packages/react-router/lib/dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ interface SharedSubmitOptions {
* Enable flushSync for this submission's state updates
*/
flushSync?: boolean;

/**
* Determine if revalidation should occur post-submission.
*/
shouldRevalidate?: boolean | (() => boolean);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/react-router/lib/dom/lib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1788,6 +1788,11 @@ interface SharedFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
* then this form will not do anything.
*/
onSubmit?: React.FormEventHandler<HTMLFormElement>;

/**
* Determine if revalidation should occur post-submission.
*/
shouldRevalidate?: boolean | (() => boolean);
}

/**
Expand Down Expand Up @@ -1911,6 +1916,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.shouldRevalidate} shouldRevalidate n/a
* @returns A progressively enhanced [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) component
*/
export const Form = React.forwardRef<HTMLFormElement, FormProps>(
Expand All @@ -1928,6 +1934,7 @@ export const Form = React.forwardRef<HTMLFormElement, FormProps>(
relative,
preventScrollReset,
viewTransition,
shouldRevalidate,
...props
},
forwardedRef,
Expand Down Expand Up @@ -1960,6 +1967,7 @@ export const Form = React.forwardRef<HTMLFormElement, FormProps>(
relative,
preventScrollReset,
viewTransition,
shouldRevalidate,
});
};

Expand Down Expand Up @@ -2549,6 +2557,7 @@ export function useSubmit(): SubmitFunction {
if (options.navigate === false) {
let key = options.fetcherKey || getUniqueFetcherId();
await router.fetch(key, currentRouteId, options.action || action, {
shouldRevalidate: options.shouldRevalidate,
preventScrollReset: options.preventScrollReset,
formData,
body,
Expand All @@ -2558,6 +2567,7 @@ export function useSubmit(): SubmitFunction {
});
} else {
await router.navigate(options.action || action, {
shouldRevalidate: options.shouldRevalidate,
preventScrollReset: options.preventScrollReset,
formData,
body,
Expand Down
43 changes: 42 additions & 1 deletion packages/react-router/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ type BaseNavigateOrFetchOptions = {
preventScrollReset?: boolean;
relative?: RelativeRoutingType;
flushSync?: boolean;
shouldRevalidate?: boolean | (() => boolean);
};

// Only allowed for navigations
Expand Down Expand Up @@ -1548,6 +1549,14 @@ export function createRouter(init: RouterInit): Router {
return;
}

let shouldRevalidate =
opts && "shouldRevalidate" in opts
? typeof opts.shouldRevalidate === "function"
? opts.shouldRevalidate()
: // undefined should eval to true
opts.shouldRevalidate !== false
: true;

await startNavigation(historyAction, nextLocation, {
submission,
// Send through the formData serialization error if we have one so we can
Expand All @@ -1556,6 +1565,7 @@ export function createRouter(init: RouterInit): Router {
preventScrollReset,
replace: opts && opts.replace,
enableViewTransition: opts && opts.viewTransition,
shouldRevalidate,
flushSync,
});
}
Expand Down Expand Up @@ -1632,6 +1642,7 @@ export function createRouter(init: RouterInit): Router {
replace?: boolean;
enableViewTransition?: boolean;
flushSync?: boolean;
shouldRevalidate?: boolean;
},
): Promise<void> {
// Abort any in-progress navigations and start a new one. Unset any ongoing
Expand Down Expand Up @@ -1771,6 +1782,15 @@ export function createRouter(init: RouterInit): Router {

matches = actionResult.matches || matches;
pendingActionResult = actionResult.pendingActionResult;

if (opts.shouldRevalidate === false) {
completeNavigation(location, {
matches,
...getActionDataForCommit(pendingActionResult),
});
return;
}

loadingNavigation = getLoadingNavigation(location, opts.submission);
flushSync = false;
// No need to do fog of war matching again on loader execution
Expand Down Expand Up @@ -2346,6 +2366,13 @@ export function createRouter(init: RouterInit): Router {
let preventScrollReset = (opts && opts.preventScrollReset) === true;

if (submission && isMutationMethod(submission.formMethod)) {
let shouldRevalidate =
opts && "shouldRevalidate" in opts
? typeof opts.shouldRevalidate === "function"
? opts.shouldRevalidate()
: // undefined should eval to true
opts.shouldRevalidate !== false
: true;
await handleFetcherAction(
key,
routeId,
Expand All @@ -2356,6 +2383,7 @@ export function createRouter(init: RouterInit): Router {
flushSync,
preventScrollReset,
submission,
shouldRevalidate,
);
return;
}
Expand Down Expand Up @@ -2388,6 +2416,7 @@ export function createRouter(init: RouterInit): Router {
flushSync: boolean,
preventScrollReset: boolean,
submission: Submission,
shouldRevalidate: boolean,
) {
interruptActiveLoads();
fetchLoadMatches.delete(key);
Expand Down Expand Up @@ -2563,6 +2592,7 @@ export function createRouter(init: RouterInit): Router {
basename,
init.patchRoutesOnNavigation != null,
[match.route.id, actionResult],
shouldRevalidate,
);

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

if (!shouldRevalidate) {
if (state.fetchers.has(key)) {
let doneFetcher = getDoneFetcher(actionResult.data);
state.fetchers.set(key, doneFetcher);
}
fetchControllers.delete(key);
return;
}

let { loaderResults, fetcherResults } =
await callLoadersAndMaybeResolveData(
dsMatches,
Expand Down Expand Up @@ -4820,6 +4859,7 @@ function getMatchesToLoad(
basename: string | undefined,
hasPatchRoutesOnNavigation: boolean,
pendingActionResult?: PendingActionResult,
shouldRevalidate?: boolean,
): {
dsMatches: DataStrategyMatch[];
revalidatingFetchers: RevalidatingFetcher[];
Expand Down Expand Up @@ -4855,7 +4895,8 @@ function getMatchesToLoad(
let actionStatus = pendingActionResult
? pendingActionResult[1].statusCode
: undefined;
let shouldSkipRevalidation = actionStatus && actionStatus >= 400;
let shouldSkipRevalidation =
(actionStatus && actionStatus >= 400) || shouldRevalidate === false;

let baseShouldRevalidateArgs = {
currentUrl,
Expand Down