react-router 8.3.1 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/development/dom-export.js +1 -1
- package/dist/development/index-react-server-client.js +1 -1
- package/dist/development/index-react-server.d.ts +42 -48
- package/dist/development/index-react-server.js +75 -31
- package/dist/development/index.d.ts +2 -2
- package/dist/development/index.js +3 -3
- package/dist/development/lib/actions.js +1 -1
- package/dist/development/lib/components.js +45 -8
- package/dist/development/lib/context.d.ts +13 -5
- package/dist/development/lib/context.js +11 -3
- package/dist/development/lib/dom/dom.js +1 -1
- package/dist/development/lib/dom/lib.js +14 -32
- package/dist/development/lib/dom/server.d.ts +6 -1
- package/dist/development/lib/dom/server.js +40 -14
- package/dist/development/lib/dom/ssr/components.js +15 -23
- package/dist/development/lib/dom/ssr/data.js +1 -1
- package/dist/development/lib/dom/ssr/entry.d.ts +4 -0
- package/dist/development/lib/dom/ssr/errorBoundaries.js +1 -1
- package/dist/development/lib/dom/ssr/fallback.js +1 -1
- package/dist/development/lib/dom/ssr/fog-of-war.js +33 -12
- package/dist/development/lib/dom/ssr/hydration.d.ts +3 -1
- package/dist/development/lib/dom/ssr/hydration.js +6 -4
- package/dist/development/lib/dom/ssr/invariant.js +1 -1
- package/dist/development/lib/dom/ssr/links.js +1 -1
- package/dist/development/lib/dom/ssr/markup.js +5 -2
- package/dist/development/lib/dom/ssr/routeModules.js +1 -1
- package/dist/development/lib/dom/ssr/routes-test-stub.js +1 -1
- package/dist/development/lib/dom/ssr/routes.js +1 -1
- package/dist/development/lib/dom/ssr/server.js +2 -2
- package/dist/development/lib/dom/ssr/single-fetch.js +10 -19
- package/dist/development/lib/dom-export/dom-router-provider.js +1 -1
- package/dist/development/lib/dom-export/hydrated-router.js +2 -1
- package/dist/development/lib/errors.js +1 -1
- package/dist/development/lib/hooks.js +49 -44
- package/dist/development/lib/href.js +1 -1
- package/dist/development/lib/router/history.js +1 -1
- package/dist/development/lib/router/instrumentation.js +1 -1
- package/dist/development/lib/router/matcher-route-pattern.d.ts +2 -0
- package/dist/development/lib/router/matcher-route-pattern.js +216 -0
- package/dist/development/lib/router/matcher-route-pattern.preload.d.ts +20 -0
- package/dist/development/lib/router/matcher-route-pattern.preload.js +36 -0
- package/dist/development/lib/router/matcher.d.ts +2 -0
- package/dist/development/lib/router/matcher.js +29 -0
- package/dist/development/lib/router/navigation.js +1 -1
- package/dist/development/lib/router/router.d.ts +13 -7
- package/dist/development/lib/router/router.js +81 -66
- package/dist/development/lib/router/url.js +1 -1
- package/dist/development/lib/router/utils.d.ts +6 -1
- package/dist/development/lib/router/utils.js +6 -4
- package/dist/development/lib/rsc/browser.js +2 -1
- package/dist/development/lib/rsc/errorBoundaries.js +1 -1
- package/dist/development/lib/rsc/html-stream/browser.js +1 -1
- package/dist/development/lib/rsc/html-stream/server.js +2 -4
- package/dist/development/lib/rsc/route-modules.js +1 -1
- package/dist/development/lib/rsc/server.rsc.d.ts +7 -0
- package/dist/development/lib/rsc/server.ssr.js +25 -21
- package/dist/development/lib/server-runtime/cookies.js +1 -1
- package/dist/development/lib/server-runtime/crypto.js +1 -1
- package/dist/development/lib/server-runtime/data.js +1 -1
- package/dist/development/lib/server-runtime/dev.js +1 -1
- package/dist/development/lib/server-runtime/entry.js +1 -1
- package/dist/development/lib/server-runtime/errors.js +1 -1
- package/dist/development/lib/server-runtime/headers.js +1 -1
- package/dist/development/lib/server-runtime/invariant.js +1 -1
- package/dist/development/lib/server-runtime/mode.js +1 -1
- package/dist/development/lib/server-runtime/routeMatching.js +3 -4
- package/dist/development/lib/server-runtime/routes.js +1 -1
- package/dist/development/lib/server-runtime/server.js +7 -6
- package/dist/development/lib/server-runtime/serverHandoff.js +1 -1
- package/dist/development/lib/server-runtime/sessions/cookieStorage.js +1 -1
- package/dist/development/lib/server-runtime/sessions/memoryStorage.js +1 -1
- package/dist/development/lib/server-runtime/sessions.js +1 -1
- package/dist/development/lib/server-runtime/single-fetch.js +1 -1
- package/dist/development/lib/server-runtime/urls.js +1 -1
- package/dist/development/lib/server-runtime/warnings.js +1 -1
- package/dist/development/lib/types/internal.js +1 -1
- package/dist/development/route-pattern.d.ts +3 -0
- package/dist/development/route-pattern.js +12 -0
- package/dist/development/vendor/turbo-stream-v2/flatten.js +1 -1
- package/dist/development/vendor/turbo-stream-v2/turbo-stream.js +1 -1
- package/dist/development/vendor/turbo-stream-v2/unflatten.js +1 -1
- package/dist/development/vendor/turbo-stream-v2/utils.js +1 -1
- package/dist/production/dom-export.js +1 -1
- package/dist/production/index-react-server-client.js +1 -1
- package/dist/production/index-react-server.d.ts +42 -48
- package/dist/production/index-react-server.js +75 -31
- package/dist/production/index.d.ts +2 -2
- package/dist/production/index.js +3 -3
- package/dist/production/lib/actions.js +1 -1
- package/dist/production/lib/components.js +45 -8
- package/dist/production/lib/context.d.ts +13 -5
- package/dist/production/lib/context.js +11 -3
- package/dist/production/lib/dom/dom.js +1 -1
- package/dist/production/lib/dom/lib.js +14 -32
- package/dist/production/lib/dom/server.d.ts +6 -1
- package/dist/production/lib/dom/server.js +40 -14
- package/dist/production/lib/dom/ssr/components.js +15 -23
- package/dist/production/lib/dom/ssr/data.js +1 -1
- package/dist/production/lib/dom/ssr/entry.d.ts +4 -0
- package/dist/production/lib/dom/ssr/errorBoundaries.js +1 -1
- package/dist/production/lib/dom/ssr/fallback.js +1 -1
- package/dist/production/lib/dom/ssr/fog-of-war.js +33 -12
- package/dist/production/lib/dom/ssr/hydration.d.ts +3 -1
- package/dist/production/lib/dom/ssr/hydration.js +6 -4
- package/dist/production/lib/dom/ssr/invariant.js +1 -1
- package/dist/production/lib/dom/ssr/links.js +1 -1
- package/dist/production/lib/dom/ssr/markup.js +5 -2
- package/dist/production/lib/dom/ssr/routeModules.js +1 -1
- package/dist/production/lib/dom/ssr/routes-test-stub.js +1 -1
- package/dist/production/lib/dom/ssr/routes.js +1 -1
- package/dist/production/lib/dom/ssr/server.js +2 -2
- package/dist/production/lib/dom/ssr/single-fetch.js +10 -19
- package/dist/production/lib/dom-export/dom-router-provider.js +1 -1
- package/dist/production/lib/dom-export/hydrated-router.js +2 -1
- package/dist/production/lib/errors.js +1 -1
- package/dist/production/lib/hooks.js +49 -44
- package/dist/production/lib/href.js +1 -1
- package/dist/production/lib/router/history.js +1 -1
- package/dist/production/lib/router/instrumentation.js +1 -1
- package/dist/production/lib/router/matcher-route-pattern.d.ts +2 -0
- package/dist/production/lib/router/matcher-route-pattern.js +216 -0
- package/dist/production/lib/router/matcher-route-pattern.preload.d.ts +20 -0
- package/dist/production/lib/router/matcher-route-pattern.preload.js +36 -0
- package/dist/production/lib/router/matcher.d.ts +2 -0
- package/dist/production/lib/router/matcher.js +29 -0
- package/dist/production/lib/router/navigation.js +1 -1
- package/dist/production/lib/router/router.d.ts +13 -7
- package/dist/production/lib/router/router.js +81 -66
- package/dist/production/lib/router/url.js +1 -1
- package/dist/production/lib/router/utils.d.ts +6 -1
- package/dist/production/lib/router/utils.js +6 -4
- package/dist/production/lib/rsc/browser.js +2 -1
- package/dist/production/lib/rsc/errorBoundaries.js +1 -1
- package/dist/production/lib/rsc/html-stream/browser.js +1 -1
- package/dist/production/lib/rsc/html-stream/server.js +2 -4
- package/dist/production/lib/rsc/route-modules.js +1 -1
- package/dist/production/lib/rsc/server.rsc.d.ts +7 -0
- package/dist/production/lib/rsc/server.ssr.js +25 -21
- package/dist/production/lib/server-runtime/cookies.js +1 -1
- package/dist/production/lib/server-runtime/crypto.js +1 -1
- package/dist/production/lib/server-runtime/data.js +1 -1
- package/dist/production/lib/server-runtime/dev.js +1 -1
- package/dist/production/lib/server-runtime/entry.js +1 -1
- package/dist/production/lib/server-runtime/errors.js +1 -1
- package/dist/production/lib/server-runtime/headers.js +1 -1
- package/dist/production/lib/server-runtime/invariant.js +1 -1
- package/dist/production/lib/server-runtime/mode.js +1 -1
- package/dist/production/lib/server-runtime/routeMatching.js +3 -4
- package/dist/production/lib/server-runtime/routes.js +1 -1
- package/dist/production/lib/server-runtime/server.js +7 -6
- package/dist/production/lib/server-runtime/serverHandoff.js +1 -1
- package/dist/production/lib/server-runtime/sessions/cookieStorage.js +1 -1
- package/dist/production/lib/server-runtime/sessions/memoryStorage.js +1 -1
- package/dist/production/lib/server-runtime/sessions.js +1 -1
- package/dist/production/lib/server-runtime/single-fetch.js +1 -1
- package/dist/production/lib/server-runtime/urls.js +1 -1
- package/dist/production/lib/server-runtime/warnings.js +1 -1
- package/dist/production/lib/types/internal.js +1 -1
- package/dist/production/route-pattern.d.ts +3 -0
- package/dist/production/route-pattern.js +12 -0
- package/dist/production/vendor/turbo-stream-v2/flatten.js +1 -1
- package/dist/production/vendor/turbo-stream-v2/turbo-stream.js +1 -1
- package/dist/production/vendor/turbo-stream-v2/unflatten.js +1 -1
- package/dist/production/vendor/turbo-stream-v2/utils.js +1 -1
- package/docs/how-to/fetchers.md +6 -1
- package/docs/how-to/middleware.md +3 -1
- package/docs/how-to/optimize-revalidation.md +178 -5
- package/docs/how-to/react-server-components.md +26 -3
- package/docs/how-to/resource-routes.md +3 -0
- package/docs/start/data/actions.md +2 -1
- package/docs/start/data/route-object.md +21 -6
- package/docs/start/framework/actions.md +4 -2
- package/docs/start/framework/route-module.md +32 -5
- package/docs/upgrading/future.md +68 -0
- package/package.json +11 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* react-router v8.
|
|
2
|
+
* react-router v8.4.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
import { isRouteErrorResponse } from "../router/utils.js";
|
|
12
|
-
import { hasInvalidProtocol } from "../router/router.js";
|
|
12
|
+
import { createDataRouteMatcher, hasInvalidProtocol } from "../router/router.js";
|
|
13
13
|
import { RSCRouterContext } from "../context.js";
|
|
14
14
|
import { decodeRedirectErrorDigest, decodeRouteErrorResponseDigest } from "../errors.js";
|
|
15
|
-
import {
|
|
15
|
+
import { escapeAttribute } from "../dom/ssr/markup.js";
|
|
16
16
|
import { shouldHydrateRouteLoader } from "../dom/ssr/routes.js";
|
|
17
17
|
import { FrameworkContext } from "../dom/ssr/components.js";
|
|
18
18
|
import { StaticRouterProvider, createStaticRouter } from "../dom/server.js";
|
|
@@ -179,7 +179,7 @@ async function routeRSCServerRequest({ request, serverResponse, createFromReadab
|
|
|
179
179
|
const redirectTransform = new TransformStream({ flush(controller) {
|
|
180
180
|
if (renderRedirect) {
|
|
181
181
|
if (hasInvalidProtocol(renderRedirect.location)) return;
|
|
182
|
-
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${
|
|
182
|
+
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeAttribute(renderRedirect.location)}"/>`));
|
|
183
183
|
}
|
|
184
184
|
} });
|
|
185
185
|
if (!hydrate) return new Response(html.pipeThrough(redirectTransform), {
|
|
@@ -258,7 +258,7 @@ async function routeRSCServerRequest({ request, serverResponse, createFromReadab
|
|
|
258
258
|
const retryRedirectTransform = new TransformStream({ flush(controller) {
|
|
259
259
|
if (retryRedirect) {
|
|
260
260
|
if (hasInvalidProtocol(retryRedirect.location)) return;
|
|
261
|
-
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${
|
|
261
|
+
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeAttribute(retryRedirect.location)}"/>`));
|
|
262
262
|
}
|
|
263
263
|
} });
|
|
264
264
|
if (!hydrate) return new Response(html.pipeThrough(retryRedirectTransform), {
|
|
@@ -333,6 +333,24 @@ function RSCStaticRouter({ getPayload, nonce }) {
|
|
|
333
333
|
if (payload.type !== "render") return null;
|
|
334
334
|
let patchedLoaderData = { ...payload.loaderData };
|
|
335
335
|
for (const match of payload.matches) if (shouldHydrateRouteLoader(match.id, match.clientLoader, match.hasLoader, false) && (match.hydrateFallbackElement || !match.hasLoader)) delete patchedLoaderData[match.id];
|
|
336
|
+
const routes = payload.matches.reduceRight((previous, match) => {
|
|
337
|
+
const route = {
|
|
338
|
+
id: match.id,
|
|
339
|
+
action: match.hasAction || !!match.clientAction,
|
|
340
|
+
element: match.element,
|
|
341
|
+
errorElement: match.errorElement,
|
|
342
|
+
handle: match.handle,
|
|
343
|
+
hydrateFallbackElement: match.hydrateFallbackElement,
|
|
344
|
+
index: match.index,
|
|
345
|
+
loader: match.hasLoader || !!match.clientLoader,
|
|
346
|
+
path: match.path,
|
|
347
|
+
shouldRevalidate: match.shouldRevalidate
|
|
348
|
+
};
|
|
349
|
+
if (previous.length > 0) route.children = previous;
|
|
350
|
+
return [route];
|
|
351
|
+
}, []);
|
|
352
|
+
const dataRouteMatcher = createDataRouteMatcher({}, payload.basename || "/");
|
|
353
|
+
dataRouteMatcher.update(routes);
|
|
336
354
|
const context = {
|
|
337
355
|
get _deepestRenderedBoundaryId() {
|
|
338
356
|
return decoded._deepestRenderedBoundaryId ?? null;
|
|
@@ -348,6 +366,7 @@ function RSCStaticRouter({ getPayload, nonce }) {
|
|
|
348
366
|
loaderHeaders: {},
|
|
349
367
|
location: payload.location,
|
|
350
368
|
statusCode: 200,
|
|
369
|
+
_match: (locationArg) => dataRouteMatcher.match(locationArg),
|
|
351
370
|
matches: payload.matches.map((match) => ({
|
|
352
371
|
params: match.params,
|
|
353
372
|
pathname: match.pathname,
|
|
@@ -363,22 +382,7 @@ function RSCStaticRouter({ getPayload, nonce }) {
|
|
|
363
382
|
}
|
|
364
383
|
}))
|
|
365
384
|
};
|
|
366
|
-
const router = createStaticRouter(
|
|
367
|
-
const route = {
|
|
368
|
-
id: match.id,
|
|
369
|
-
action: match.hasAction || !!match.clientAction,
|
|
370
|
-
element: match.element,
|
|
371
|
-
errorElement: match.errorElement,
|
|
372
|
-
handle: match.handle,
|
|
373
|
-
hydrateFallbackElement: match.hydrateFallbackElement,
|
|
374
|
-
index: match.index,
|
|
375
|
-
loader: match.hasLoader || !!match.clientLoader,
|
|
376
|
-
path: match.path,
|
|
377
|
-
shouldRevalidate: match.shouldRevalidate
|
|
378
|
-
};
|
|
379
|
-
if (previous.length > 0) route.children = previous;
|
|
380
|
-
return [route];
|
|
381
|
-
}, []), context);
|
|
385
|
+
const router = createStaticRouter(routes, context);
|
|
382
386
|
const frameworkContext = {
|
|
383
387
|
future: {},
|
|
384
388
|
isSpaMode: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* react-router v8.
|
|
2
|
+
* react-router v8.4.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import { matchRoutesImpl } from "../router/utils.js";
|
|
12
11
|
import invariant from "./invariant.js";
|
|
13
12
|
//#region lib/server-runtime/routeMatching.ts
|
|
14
|
-
function matchServerRoutes(manifest,
|
|
15
|
-
let matches =
|
|
13
|
+
function matchServerRoutes(manifest, staticHandler, pathname) {
|
|
14
|
+
let matches = staticHandler.match(pathname);
|
|
16
15
|
if (!matches) return null;
|
|
17
16
|
return matches.map((match) => {
|
|
18
17
|
let route = manifest[match.route.id];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* react-router v8.
|
|
2
|
+
* react-router v8.4.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -90,12 +90,12 @@ function derive(build, mode) {
|
|
|
90
90
|
}
|
|
91
91
|
let manifestUrl = getManifestPath(build.routeDiscovery.manifestPath, build.basename);
|
|
92
92
|
if (build.routeDiscovery.mode === "lazy" && requestUrl.pathname === manifestUrl) try {
|
|
93
|
-
return await handleManifestRequest(build, staticHandler
|
|
93
|
+
return await handleManifestRequest(build, staticHandler, requestUrl);
|
|
94
94
|
} catch (e) {
|
|
95
95
|
handleError(e);
|
|
96
96
|
return new Response("Unknown Server Error", { status: 500 });
|
|
97
97
|
}
|
|
98
|
-
let matches = matchServerRoutes(build.routes, staticHandler
|
|
98
|
+
let matches = matchServerRoutes(build.routes, staticHandler, normalizedPathname);
|
|
99
99
|
if (matches && matches.length > 0) Object.assign(params, matches[0].params);
|
|
100
100
|
if (requestHandlerInstrumentations?.length) loadContext.set(instrumentationResultMetaContext, {
|
|
101
101
|
url: createDataFunctionUrl(request, normalizedPath),
|
|
@@ -173,7 +173,7 @@ const createRequestHandler = (build, mode) => {
|
|
|
173
173
|
return _requestHandler(request, initialContext);
|
|
174
174
|
};
|
|
175
175
|
};
|
|
176
|
-
async function handleManifestRequest(build,
|
|
176
|
+
async function handleManifestRequest(build, staticHandler, url) {
|
|
177
177
|
if (url.toString().length > 7680) return new Response(null, {
|
|
178
178
|
statusText: "Bad Request",
|
|
179
179
|
status: 400
|
|
@@ -188,7 +188,7 @@ async function handleManifestRequest(build, dataRoutes, branches, url) {
|
|
|
188
188
|
let paths = new Set(pathParam.split(",").filter(Boolean));
|
|
189
189
|
for (let path of paths) {
|
|
190
190
|
if (!path.startsWith("/")) path = `/${path}`;
|
|
191
|
-
let matches = matchServerRoutes(build.routes,
|
|
191
|
+
let matches = matchServerRoutes(build.routes, staticHandler, path);
|
|
192
192
|
if (matches) for (let match of matches) {
|
|
193
193
|
let routeId = match.route.id;
|
|
194
194
|
let route = build.assets.routes[routeId];
|
|
@@ -256,7 +256,7 @@ async function handleDocumentRequest(serverMode, build, staticHandler, request,
|
|
|
256
256
|
};
|
|
257
257
|
let entryContext = {
|
|
258
258
|
manifest: build.assets,
|
|
259
|
-
branches:
|
|
259
|
+
branches: [],
|
|
260
260
|
routeModules: createEntryRouteModules(build.routes),
|
|
261
261
|
staticHandlerContext: context,
|
|
262
262
|
criticalCss,
|
|
@@ -291,6 +291,7 @@ async function handleDocumentRequest(serverMode, build, staticHandler, request,
|
|
|
291
291
|
};
|
|
292
292
|
entryContext = {
|
|
293
293
|
...entryContext,
|
|
294
|
+
branches: [],
|
|
294
295
|
staticHandlerContext: context,
|
|
295
296
|
serverHandoffString: createServerHandoffString(baseServerHandoff),
|
|
296
297
|
serverHandoffStream: encodeViaTurboStream(state, request.signal, build.entry.module.streamTimeout, serverMode),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-router v8.4.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Remix Software Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE.md file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import { unstable_preloadRoutePattern } from "./lib/router/matcher-route-pattern.preload.js";
|
|
12
|
+
export { unstable_preloadRoutePattern };
|
package/docs/how-to/fetchers.md
CHANGED
|
@@ -85,7 +85,7 @@ export default function Component() {
|
|
|
85
85
|
|
|
86
86
|
### 3. Submit the form
|
|
87
87
|
|
|
88
|
-
If you submit the form now, the fetcher will call the action and revalidate the route data automatically.
|
|
88
|
+
If you submit the form now, the fetcher will call the action and revalidate the route data automatically. See [Revalidation Optimization](./optimize-revalidation) to skip that reload for a route or a single submission.
|
|
89
89
|
|
|
90
90
|
### 4. Render pending state
|
|
91
91
|
|
|
@@ -305,3 +305,8 @@ Fetchers can be submitted programmatically with `fetcher.submit`:
|
|
|
305
305
|
```
|
|
306
306
|
|
|
307
307
|
Note the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading its attributes and serializing the data from its elements.
|
|
308
|
+
|
|
309
|
+
`fetcher.submit` accepts the same [`defaultShouldRevalidate`][fetcher-submit-options] option as [`<Form>`][form]. Pass `false` to skip loader revalidation for that submission. See [Revalidation Optimization](./optimize-revalidation) for how that interacts with route-level `shouldRevalidate`.
|
|
310
|
+
|
|
311
|
+
[fetcher-submit-options]: https://api.reactrouter.com/v8/interfaces/react-router.FetcherSubmitOptions.html#defaultshouldrevalidate
|
|
312
|
+
[form]: ../api/components/Form#defaultshouldrevalidate
|
|
@@ -358,7 +358,9 @@ In theory, we could have leveraged [`AsyncLocalStorage`][asynclocalstorage] dire
|
|
|
358
358
|
|
|
359
359
|
That said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API:
|
|
360
360
|
|
|
361
|
-
<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and
|
|
361
|
+
<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and route actions because they run in the same server execution context 🤯</docs-info>
|
|
362
|
+
|
|
363
|
+
<docs-warning>Do not rely on route middleware to provide access control for React Server Functions. Server Functions are not inherently associated with a route, and a client can call the same Server Function through a URL with different middleware. Server Functions must perform all of their own access control checks. Use a route `action` instead when you want middleware-driven access control.</docs-warning>
|
|
362
364
|
|
|
363
365
|
```tsx filename=app/user-context.ts
|
|
364
366
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -1,12 +1,185 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Revalidation Optimization
|
|
3
|
-
hidden: true
|
|
4
3
|
---
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
# Revalidation Optimization
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
[MODES: framework, data]
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
<br/>
|
|
10
|
+
<br/>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
After a mutation or some navigations, React Router re-runs loaders so
|
|
13
|
+
the UI stays in sync with the server. That default is the right
|
|
14
|
+
starting point. When a loader is expensive, or a mutation cannot
|
|
15
|
+
affect that route's data, you can skip the reload.
|
|
16
|
+
|
|
17
|
+
<docs-warning>
|
|
18
|
+
Skipping revalidation can leave the UI out of sync with the server.
|
|
19
|
+
Prefer targeting a specific action or navigation, and fall back to
|
|
20
|
+
`defaultShouldRevalidate` instead of always returning `false`.
|
|
21
|
+
</docs-warning>
|
|
22
|
+
|
|
23
|
+
## Default behavior
|
|
24
|
+
|
|
25
|
+
The default behavior differs between Framework and Data Modes:
|
|
26
|
+
|
|
27
|
+
- **Framework Mode with SSR**
|
|
28
|
+
- Defaults to opt-out behavior - active loaders are revalidated on navigations and successful submissions ([`Link`][link], [`Form`][form], [`fetcher.submit`](fetcher-submit))
|
|
29
|
+
- Failed submissions returning a 4xx/5xx status do not trigger revalidations by default
|
|
30
|
+
- **Framework "SPA Mode" and Data Mode**
|
|
31
|
+
- Defaults to opt-out behavior on successful submissions - active loaders are revalidated on successful submissions ([`Form`][form], [`fetcher.submit`])
|
|
32
|
+
- Failed submissions returning a 4xx/5xx status do not trigger revalidations by default
|
|
33
|
+
- Defaults to opt-in behavior for GET navigations ([`Link`][link]) - active loaders are only revalidated if their dynamic params changed, or if any search params changed
|
|
34
|
+
- A GET navigation to the _exact_ same URL is treated like a page refresh and all loaders are revalidated.
|
|
35
|
+
|
|
36
|
+
Matched matched routes are handled independently - A child that skips
|
|
37
|
+
revalidation does not skip any ancestor routes.
|
|
38
|
+
|
|
39
|
+
[`fetcher.load`][use-fetcher] only revalidates by default after action
|
|
40
|
+
submissions and explicit [`useRevalidator`][use-revalidator] calls, not
|
|
41
|
+
on search-param or param-driven navigations.
|
|
42
|
+
|
|
43
|
+
A plain `fetch()` to a [resource route][resource-routes] does not
|
|
44
|
+
go through the router, so it does not revalidate loaders.
|
|
45
|
+
|
|
46
|
+
## Skip a route with `shouldRevalidate`
|
|
47
|
+
|
|
48
|
+
Export `shouldRevalidate` from the [route module][route-module]
|
|
49
|
+
(Framework Mode) or set it on the [route object][data-mode]
|
|
50
|
+
(Data Mode). Returning `false` skips **that route's** loader.
|
|
51
|
+
|
|
52
|
+
```tsx filename=app/routes/dashboard.tsx
|
|
53
|
+
// Framework Mode
|
|
54
|
+
export function shouldRevalidate() {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```tsx src/main.tsx
|
|
60
|
+
// Data Mode
|
|
61
|
+
createBrowserRouter([
|
|
62
|
+
{
|
|
63
|
+
path: "/dashboard",
|
|
64
|
+
loader: dashboardLoader,
|
|
65
|
+
shouldRevalidate: () => false,
|
|
66
|
+
Component: Dashboard,
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Always returning `false` opts that route out of the default
|
|
72
|
+
behavior completely, including cases you usually still want
|
|
73
|
+
(param changes, explicit [`useRevalidator`][use-revalidator]).
|
|
74
|
+
Prefer the conditional form below.
|
|
75
|
+
|
|
76
|
+
## Opt out of specific requests
|
|
77
|
+
|
|
78
|
+
Inspect
|
|
79
|
+
[`ShouldRevalidateFunctionArgs`][should-revalidate-args]
|
|
80
|
+
and return `defaultShouldRevalidate` for everything else.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import type { ShouldRevalidateFunctionArgs } from "react-router";
|
|
84
|
+
|
|
85
|
+
export function shouldRevalidate({
|
|
86
|
+
formMethod,
|
|
87
|
+
formAction,
|
|
88
|
+
defaultShouldRevalidate,
|
|
89
|
+
}: ShouldRevalidateFunctionArgs) {
|
|
90
|
+
if (
|
|
91
|
+
formMethod === "POST" &&
|
|
92
|
+
formAction?.endsWith("/analytics")
|
|
93
|
+
) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return defaultShouldRevalidate;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Other useful fields:
|
|
102
|
+
|
|
103
|
+
- `formData`, `json`, `text` — the submission body
|
|
104
|
+
- `actionResult`, `actionStatus` — the action's return value
|
|
105
|
+
- `currentUrl`, `nextUrl`, `currentParams`, `nextParams` —
|
|
106
|
+
the navigation
|
|
107
|
+
|
|
108
|
+
You can ignore search-param-only updates while still
|
|
109
|
+
revalidating when the pathname changes:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
export function shouldRevalidate({
|
|
113
|
+
currentUrl,
|
|
114
|
+
nextUrl,
|
|
115
|
+
defaultShouldRevalidate,
|
|
116
|
+
}: ShouldRevalidateFunctionArgs) {
|
|
117
|
+
if (currentUrl.pathname === nextUrl.pathname) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return defaultShouldRevalidate;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Skip revalidation for one event
|
|
126
|
+
|
|
127
|
+
Pass `defaultShouldRevalidate={false}` at the call site so you
|
|
128
|
+
do not have to change every route file. This works on
|
|
129
|
+
[`<Form>`][form], [`<Link>`][link], `<fetcher.Form>`, and as an
|
|
130
|
+
option to [`useSubmit`][use-submit], `fetcher.submit`,
|
|
131
|
+
[`useNavigate`][use-navigate], and
|
|
132
|
+
[`useSearchParams`][use-search-params].
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { Form, Link } from "react-router";
|
|
136
|
+
|
|
137
|
+
<Link
|
|
138
|
+
to="/search?q=shoes"
|
|
139
|
+
defaultShouldRevalidate={false}
|
|
140
|
+
>
|
|
141
|
+
Search Shoes
|
|
142
|
+
</Link>
|
|
143
|
+
|
|
144
|
+
<Form
|
|
145
|
+
method="post"
|
|
146
|
+
action="/analytics"
|
|
147
|
+
defaultShouldRevalidate={false}
|
|
148
|
+
>
|
|
149
|
+
<button>Track Click</button>
|
|
150
|
+
</Form>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
fetcher.submit(
|
|
155
|
+
{ intent: "save-progress" },
|
|
156
|
+
{
|
|
157
|
+
method: "post",
|
|
158
|
+
action: "/save-progress",
|
|
159
|
+
defaultShouldRevalidate: false,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If a matched route does **not** export `shouldRevalidate`, this
|
|
165
|
+
value is used directly for that loader. If it **does** export
|
|
166
|
+
`shouldRevalidate`, the value is passed in as
|
|
167
|
+
`defaultShouldRevalidate` and the route still has the final say.
|
|
168
|
+
|
|
169
|
+
That is why a child `shouldRevalidate` that always returns
|
|
170
|
+
`false` cannot hide a root reload after `fetcher.submit`. Either
|
|
171
|
+
also opt `root` out for that case, or pass
|
|
172
|
+
`defaultShouldRevalidate: false` at the call site when `root`
|
|
173
|
+
has no `shouldRevalidate` of its own.
|
|
174
|
+
|
|
175
|
+
[data-mode]: ../start/data/route-object#shouldrevalidate
|
|
176
|
+
[form]: ../api/components/Form
|
|
177
|
+
[link]: ../api/components/Link
|
|
178
|
+
[resource-routes]: ./resource-routes
|
|
179
|
+
[route-module]: ../start/framework/route-module#shouldrevalidate
|
|
180
|
+
[should-revalidate-args]: https://api.reactrouter.com/v8/interfaces/react-router.ShouldRevalidateFunctionArgs.html
|
|
181
|
+
[use-fetcher]: ../api/hooks/useFetcher
|
|
182
|
+
[use-navigate]: ../api/hooks/useNavigate
|
|
183
|
+
[use-revalidator]: ../api/hooks/useRevalidator
|
|
184
|
+
[use-search-params]: ../api/hooks/useSearchParams
|
|
185
|
+
[use-submit]: ../api/hooks/useSubmit
|
|
@@ -498,16 +498,39 @@ Using Server Components in loaders can be helpful for incremental adoption of RS
|
|
|
498
498
|
|
|
499
499
|
[Server Functions][react-server-functions-doc] are a React feature that allow you to call async functions executed on the server. They're defined with the [`"use server"`][use-server-docs] directive.
|
|
500
500
|
|
|
501
|
+
<docs-warning>
|
|
502
|
+
|
|
503
|
+
Treat every Server Function as a public endpoint. The client controls both the
|
|
504
|
+
Server Function identifier and request URL, so do not rely on route middleware
|
|
505
|
+
for authentication or authorization. Server Functions must perform their own
|
|
506
|
+
access control and input validation; use a route `action` when access control
|
|
507
|
+
should be middleware-driven.
|
|
508
|
+
|
|
509
|
+
</docs-warning>
|
|
510
|
+
|
|
501
511
|
```tsx
|
|
502
512
|
"use server";
|
|
503
513
|
|
|
514
|
+
import { unstable_getRequest as getRequest } from "react-router";
|
|
515
|
+
import { requireUser } from "./auth.ts";
|
|
516
|
+
|
|
504
517
|
export async function updateFavorite(formData: FormData) {
|
|
505
|
-
let
|
|
518
|
+
let user = await requireUser(getRequest());
|
|
519
|
+
let movieId = Number(formData.get("id"));
|
|
506
520
|
let intent = formData.get("intent");
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
!Number.isSafeInteger(movieId) ||
|
|
524
|
+
movieId <= 0 ||
|
|
525
|
+
(intent !== "add" && intent !== "remove")
|
|
526
|
+
) {
|
|
527
|
+
throw new Error("Invalid form submission");
|
|
528
|
+
}
|
|
529
|
+
|
|
507
530
|
if (intent === "add") {
|
|
508
|
-
await addFavorite(
|
|
531
|
+
await addFavorite(user.id, movieId);
|
|
509
532
|
} else {
|
|
510
|
-
await removeFavorite(
|
|
533
|
+
await removeFavorite(user.id, movieId);
|
|
511
534
|
}
|
|
512
535
|
}
|
|
513
536
|
```
|
|
@@ -66,6 +66,8 @@ export function action(_: Route.ActionArgs) {
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
Calling this `action` through [`<Form>`][form] or [`useFetcher`][fetcher] still revalidates matched UI loaders. A plain `fetch()` to the resource URL does not. See [Revalidation Optimization][optimize-revalidation] for more info.
|
|
70
|
+
|
|
69
71
|
## Return Types
|
|
70
72
|
|
|
71
73
|
Resource Routes are flexible when it comes to the return type - you can return [`Response`][Response] instances or [`data()`][data] objects. A good general rule of thumb when deciding which type to use is:
|
|
@@ -124,3 +126,4 @@ export function action() {
|
|
|
124
126
|
[form]: ../api/components/Form
|
|
125
127
|
[await]: ../api/components/Await
|
|
126
128
|
[error-boundary]: ../start/framework/route-module#errorboundary
|
|
129
|
+
[optimize-revalidation]: ./optimize-revalidation
|