Skip to content

Commit 9c211ae

Browse files
committed
Add callsite revalidation optout to navigations
1 parent b95dff0 commit 9c211ae

File tree

4 files changed

+35
-23
lines changed

4 files changed

+35
-23
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2415,7 +2415,7 @@ describe("fetchers", () => {
24152415
});
24162416
});
24172417

2418-
it("skips all revalidation when shouldRevalidate is false", async () => {
2418+
it("skips all revalidation when callsite defaultShouldRevalidate is false", async () => {
24192419
let key = "key";
24202420
let actionKey = "actionKey";
24212421
let t = setup({

packages/react-router/lib/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ export interface NavigateOptions {
154154
flushSync?: boolean;
155155
/** 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. */
156156
viewTransition?: boolean;
157+
/** When false, suppresses loader revalidation triggered by this navigation **/
158+
defaultShouldRevalidate?: boolean;
157159
}
158160

159161
/**

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,22 @@ export interface LinkProps
12871287
* To apply specific styles for the transition, see {@link useViewTransitionState}
12881288
*/
12891289
viewTransition?: boolean;
1290+
1291+
/**
1292+
* Controls whether loaders should revalidate when this link is clicked.
1293+
*
1294+
* ```tsx
1295+
* <Link to="/some/path" defaultShouldRevalidate={false} />
1296+
* ```
1297+
*
1298+
* When set to `false`, prevents the default revalidation behavior after navigation,
1299+
* keeping the current loader data without refetching. This can be useful when updating
1300+
* search params and you don't want to trigger a revalidation.
1301+
*
1302+
* By default (when not specified), loaders will revalidate according to the framework's
1303+
* standard revalidation behavior.
1304+
*/
1305+
defaultShouldRevalidate?: boolean;
12901306
}
12911307

12921308
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
@@ -1319,6 +1335,7 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
13191335
* @param {LinkProps.state} props.state n/a
13201336
* @param {LinkProps.to} props.to n/a
13211337
* @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a
1338+
* @param {LinkProps.defaultShouldRevalidate} props.defaultShouldRevalidate n/a
13221339
*/
13231340
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
13241341
function LinkWithRef(
@@ -1334,6 +1351,7 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
13341351
to,
13351352
preventScrollReset,
13361353
viewTransition,
1354+
defaultShouldRevalidate,
13371355
...rest
13381356
},
13391357
forwardedRef,
@@ -1389,6 +1407,7 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
13891407
preventScrollReset,
13901408
relative,
13911409
viewTransition,
1410+
defaultShouldRevalidate,
13921411
});
13931412
function handleClick(
13941413
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
@@ -2183,6 +2202,7 @@ function useDataRouterState(hookName: DataRouterStateHook) {
21832202
* @param options.viewTransition Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
21842203
* for this navigation. To apply specific styles during the transition, see
21852204
* {@link useViewTransitionState}. Defaults to `false`.
2205+
* @param options.defaultShouldRevalidate Defaults to `true`
21862206
* @returns A click handler function that can be used in a custom {@link Link} component.
21872207
*/
21882208
export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
@@ -2194,13 +2214,15 @@ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
21942214
preventScrollReset,
21952215
relative,
21962216
viewTransition,
2217+
defaultShouldRevalidate,
21972218
}: {
21982219
target?: React.HTMLAttributeAnchorTarget;
21992220
replace?: boolean;
22002221
state?: any;
22012222
preventScrollReset?: boolean;
22022223
relative?: RelativeRoutingType;
22032224
viewTransition?: boolean;
2225+
defaultShouldRevalidate?: boolean;
22042226
} = {},
22052227
): (event: React.MouseEvent<E, MouseEvent>) => void {
22062228
let navigate = useNavigate();
@@ -2225,6 +2247,7 @@ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
22252247
preventScrollReset,
22262248
relative,
22272249
viewTransition,
2250+
defaultShouldRevalidate,
22282251
});
22292252
}
22302253
},
@@ -2239,6 +2262,7 @@ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
22392262
preventScrollReset,
22402263
relative,
22412264
viewTransition,
2265+
defaultShouldRevalidate,
22422266
],
22432267
);
22442268
}
@@ -2549,8 +2573,6 @@ export function useSubmit(): SubmitFunction {
25492573

25502574
return React.useCallback<SubmitFunction>(
25512575
async (target, options = {}) => {
2552-
debugger;
2553-
console.log("useSubmit", target, options);
25542576
let { action, method, encType, formData, body } = getFormSubmissionInfo(
25552577
target,
25562578
basename,

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,8 @@ export function createRouter(init: RouterInit): Router {
15111511
// action/loader this will be ignored and the redirect will be a PUSH
15121512
historyAction = NavigationType.Replace;
15131513
}
1514+
let callSiteDefaultShouldRevalidate =
1515+
opts?.defaultShouldRevalidate !== false;
15141516

15151517
let preventScrollReset =
15161518
opts && "preventScrollReset" in opts
@@ -1549,14 +1551,6 @@ export function createRouter(init: RouterInit): Router {
15491551
return;
15501552
}
15511553

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-
15601554
await startNavigation(historyAction, nextLocation, {
15611555
submission,
15621556
// Send through the formData serialization error if we have one so we can
@@ -1565,8 +1559,8 @@ export function createRouter(init: RouterInit): Router {
15651559
preventScrollReset,
15661560
replace: opts && opts.replace,
15671561
enableViewTransition: opts && opts.viewTransition,
1568-
shouldRevalidate,
15691562
flushSync,
1563+
callSiteDefaultShouldRevalidate,
15701564
});
15711565
}
15721566

@@ -1642,7 +1636,7 @@ export function createRouter(init: RouterInit): Router {
16421636
replace?: boolean;
16431637
enableViewTransition?: boolean;
16441638
flushSync?: boolean;
1645-
shouldRevalidate?: boolean;
1639+
callSiteDefaultShouldRevalidate?: boolean;
16461640
},
16471641
): Promise<void> {
16481642
// Abort any in-progress navigations and start a new one. Unset any ongoing
@@ -1782,15 +1776,6 @@ export function createRouter(init: RouterInit): Router {
17821776

17831777
matches = actionResult.matches || matches;
17841778
pendingActionResult = actionResult.pendingActionResult;
1785-
1786-
if (opts.shouldRevalidate === false) {
1787-
completeNavigation(location, {
1788-
matches,
1789-
...getActionDataForCommit(pendingActionResult),
1790-
});
1791-
return;
1792-
}
1793-
17941779
loadingNavigation = getLoadingNavigation(location, opts.submission);
17951780
flushSync = false;
17961781
// No need to do fog of war matching again on loader execution
@@ -1823,6 +1808,7 @@ export function createRouter(init: RouterInit): Router {
18231808
opts && opts.initialHydration === true,
18241809
flushSync,
18251810
pendingActionResult,
1811+
opts && opts.callSiteDefaultShouldRevalidate !== false,
18261812
);
18271813

18281814
if (shortCircuited) {
@@ -2028,6 +2014,7 @@ export function createRouter(init: RouterInit): Router {
20282014
initialHydration?: boolean,
20292015
flushSync?: boolean,
20302016
pendingActionResult?: PendingActionResult,
2017+
callSiteDefaultShouldRevalidate?: boolean,
20312018
): Promise<HandleLoadersResult> {
20322019
// Figure out the right navigation we want to use for data loading
20332020
let loadingNavigation =
@@ -2135,6 +2122,7 @@ export function createRouter(init: RouterInit): Router {
21352122
basename,
21362123
init.patchRoutesOnNavigation != null,
21372124
pendingActionResult,
2125+
callSiteDefaultShouldRevalidate,
21382126
);
21392127

21402128
pendingNavigationLoadId = ++incrementingLoadId;
@@ -2396,7 +2384,6 @@ export function createRouter(init: RouterInit): Router {
23962384
flushSync,
23972385
preventScrollReset,
23982386
submission,
2399-
// defaultShouldRevalidate, // todo
24002387
);
24012388
}
24022389

@@ -4937,6 +4924,7 @@ function getMatchesToLoad(
49374924
forceShouldLoad,
49384925
);
49394926
}
4927+
49404928
// This is the default implementation for when we revalidate. If the route
49414929
// provides it's own implementation, then we give them full control but
49424930
// provide this value so they can leverage it if needed after they check

0 commit comments

Comments
 (0)