@rangojs/router 0.21.0 → 0.22.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/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
|
@@ -7,7 +7,6 @@ import React, {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
9
|
useRef,
|
|
10
|
-
useSyncExternalStore,
|
|
11
10
|
type ForwardRefExoticComponent,
|
|
12
11
|
type RefAttributes,
|
|
13
12
|
} from "react";
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
subscribeToPrefetchScopeChange,
|
|
21
20
|
} from "../link-interceptor.js";
|
|
22
21
|
import { subscribeToLocationChange } from "../event-controller.js";
|
|
23
|
-
import { subscribeToNothing } from "./subscribe-to-nothing.js";
|
|
24
22
|
import { isLocationStateDefinition } from "./location-state-shared.js";
|
|
25
23
|
|
|
26
24
|
export type StateOrGetter<T = unknown> = T | (() => T);
|
|
@@ -39,7 +37,7 @@ import {
|
|
|
39
37
|
} from "../prefetch/loader.js";
|
|
40
38
|
import { observeForPrefetch } from "../prefetch/observer.js";
|
|
41
39
|
import {
|
|
42
|
-
|
|
40
|
+
ENVIRONMENT_DEFAULT_PREFETCH,
|
|
43
41
|
resolveAdaptiveStrategy,
|
|
44
42
|
subscribeToAdaptiveStrategyChange,
|
|
45
43
|
} from "../prefetch/default-strategy.js";
|
|
@@ -240,19 +238,11 @@ export const Link: ForwardRefExoticComponent<
|
|
|
240
238
|
ref,
|
|
241
239
|
) {
|
|
242
240
|
const ctx = useContext(NavigationStoreContext);
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
// the app root); provider-less SSR has no origin and keeps links internal.
|
|
249
|
-
const storeLocation = ctx?.eventController.getState().location as
|
|
250
|
-
| URL
|
|
251
|
-
| undefined;
|
|
252
|
-
const origin =
|
|
253
|
-
storeLocation?.origin ??
|
|
254
|
-
(typeof window !== "undefined" ? window.location.origin : undefined);
|
|
255
|
-
const isExternal = isExternalUrl(to, origin);
|
|
241
|
+
// The document's origin from the router store, the same on both sides
|
|
242
|
+
// (SSR: the live request's URL; browser: the store location), so
|
|
243
|
+
// data-external agrees across hydration. Without a provider there is no
|
|
244
|
+
// store and no origin: links stay internal.
|
|
245
|
+
const isExternal = isExternalUrl(to, ctx?.origin);
|
|
256
246
|
|
|
257
247
|
// Auto-prefix with basename for app-local paths.
|
|
258
248
|
// Skip if external, already prefixed, or not a root-relative path.
|
|
@@ -264,19 +254,11 @@ export const Link: ForwardRefExoticComponent<
|
|
|
264
254
|
return to === "/" ? bn : bn + to;
|
|
265
255
|
}, [to, isExternal, ctx?.basename]);
|
|
266
256
|
|
|
267
|
-
// No explicit `prefetch` prop: fall back to the router-wide default
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// input capability rather than a module-load snapshot.
|
|
257
|
+
// No explicit `prefetch` prop: fall back to the router-wide default from
|
|
258
|
+
// the router store. Adaptive is resolved against the input capability when a
|
|
259
|
+
// prefetch is armed or triggered, never during render.
|
|
271
260
|
const configuredStrategy =
|
|
272
|
-
prefetch ?? ctx?.defaultPrefetch ??
|
|
273
|
-
const resolvedStrategy = useSyncExternalStore(
|
|
274
|
-
configuredStrategy === "adaptive"
|
|
275
|
-
? subscribeToAdaptiveStrategyChange
|
|
276
|
-
: subscribeToNothing,
|
|
277
|
-
() => resolveAdaptiveStrategy(configuredStrategy),
|
|
278
|
-
() => (configuredStrategy === "adaptive" ? "hover" : configuredStrategy),
|
|
279
|
-
);
|
|
261
|
+
prefetch ?? ctx?.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
|
|
280
262
|
|
|
281
263
|
// Internal ref for viewport observation; merge with forwarded ref
|
|
282
264
|
const internalRef = useRef<HTMLAnchorElement | null>(null);
|
|
@@ -375,8 +357,9 @@ export const Link: ForwardRefExoticComponent<
|
|
|
375
357
|
|
|
376
358
|
const handleMouseEnter = useCallback(() => {
|
|
377
359
|
const element = internalRef.current;
|
|
360
|
+
const strategy = resolveAdaptiveStrategy(configuredStrategy);
|
|
378
361
|
if (
|
|
379
|
-
(
|
|
362
|
+
(strategy === "hover" || strategy === "viewport") &&
|
|
380
363
|
!isExternal &&
|
|
381
364
|
ctx?.store &&
|
|
382
365
|
(!element ||
|
|
@@ -395,17 +378,26 @@ export const Link: ForwardRefExoticComponent<
|
|
|
395
378
|
prefetchKey,
|
|
396
379
|
);
|
|
397
380
|
}
|
|
398
|
-
}, [
|
|
381
|
+
}, [configuredStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
399
382
|
|
|
400
383
|
// Viewport/render prefetch: waits for idle before starting,
|
|
401
384
|
// uses concurrency-limited queue to avoid flooding.
|
|
402
385
|
useEffect(() => {
|
|
403
386
|
if (isExternal || !ctx?.store) return;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
387
|
+
if (
|
|
388
|
+
configuredStrategy !== "viewport" &&
|
|
389
|
+
configuredStrategy !== "render" &&
|
|
390
|
+
configuredStrategy !== "adaptive"
|
|
391
|
+
) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
407
394
|
|
|
408
395
|
const armPrefetch = (): (() => void) => {
|
|
396
|
+
const strategy = resolveAdaptiveStrategy(configuredStrategy);
|
|
397
|
+
const isViewport = strategy === "viewport";
|
|
398
|
+
const isRender = strategy === "render";
|
|
399
|
+
if (!isViewport && !isRender) return () => {};
|
|
400
|
+
|
|
409
401
|
const element = internalRef.current;
|
|
410
402
|
if (
|
|
411
403
|
element &&
|
|
@@ -457,27 +449,30 @@ export const Link: ForwardRefExoticComponent<
|
|
|
457
449
|
};
|
|
458
450
|
|
|
459
451
|
let disarmPrefetch = armPrefetch();
|
|
452
|
+
const rearm = () => {
|
|
453
|
+
disarmPrefetch();
|
|
454
|
+
disarmPrefetch = armPrefetch();
|
|
455
|
+
};
|
|
460
456
|
const element = internalRef.current;
|
|
461
457
|
const unsubscribeScope = element
|
|
462
|
-
? subscribeToPrefetchScopeChange(element,
|
|
463
|
-
disarmPrefetch();
|
|
464
|
-
disarmPrefetch = armPrefetch();
|
|
465
|
-
})
|
|
458
|
+
? subscribeToPrefetchScopeChange(element, rearm)
|
|
466
459
|
: undefined;
|
|
467
460
|
const unsubscribeLocation = subscribeToLocationChange(
|
|
468
461
|
ctx.eventController,
|
|
469
|
-
|
|
470
|
-
disarmPrefetch();
|
|
471
|
-
disarmPrefetch = armPrefetch();
|
|
472
|
-
},
|
|
462
|
+
rearm,
|
|
473
463
|
);
|
|
464
|
+
const unsubscribeAdaptive =
|
|
465
|
+
configuredStrategy === "adaptive"
|
|
466
|
+
? subscribeToAdaptiveStrategyChange(rearm)
|
|
467
|
+
: undefined;
|
|
474
468
|
|
|
475
469
|
return () => {
|
|
476
470
|
unsubscribeScope?.();
|
|
477
471
|
unsubscribeLocation();
|
|
472
|
+
unsubscribeAdaptive?.();
|
|
478
473
|
disarmPrefetch();
|
|
479
474
|
};
|
|
480
|
-
}, [
|
|
475
|
+
}, [configuredStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
481
476
|
|
|
482
477
|
return (
|
|
483
478
|
<a
|
|
@@ -8,9 +8,11 @@ import React, {
|
|
|
8
8
|
useMemo,
|
|
9
9
|
useRef,
|
|
10
10
|
use,
|
|
11
|
+
startTransition,
|
|
11
12
|
type ReactNode,
|
|
12
13
|
} from "react";
|
|
13
14
|
import {
|
|
15
|
+
HydrationBarrierContext,
|
|
14
16
|
LocationStateContext,
|
|
15
17
|
NavigationStoreContext,
|
|
16
18
|
type NavigationStoreContextValue,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
deferredHandleNames,
|
|
38
40
|
resolveDeferredHandleValues,
|
|
39
41
|
} from "../../handles/deferred-resolution.js";
|
|
42
|
+
import { auditTreeUpdate } from "../../suspense-audit.js";
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* Process handles from an async generator, updating the event controller
|
|
@@ -278,6 +281,18 @@ export interface NavigationProviderProps {
|
|
|
278
281
|
* it to seed a nonce so components calling useNonce() can be exercised.
|
|
279
282
|
*/
|
|
280
283
|
nonce?: string;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The document this provider hydrates (initBrowserApp; renderRoute with
|
|
287
|
+
* `hydrate: true`), whose store EventController.lockHydration locked.
|
|
288
|
+
* `settled` resolves once the stream has ended and React has revealed every
|
|
289
|
+
* boundary. Until then location state is `undefined`, as SSR rendered it;
|
|
290
|
+
* then one transition gives the entry's state and changes
|
|
291
|
+
* HydrationBarrierContext, and the store is released after that commit. A
|
|
292
|
+
* navigation committed earlier releases the store before its update.
|
|
293
|
+
* Absent for a provider mounted on the client.
|
|
294
|
+
*/
|
|
295
|
+
hydration?: { settled: Promise<void> };
|
|
281
296
|
}
|
|
282
297
|
|
|
283
298
|
/**
|
|
@@ -313,14 +328,18 @@ export function NavigationProvider({
|
|
|
313
328
|
basename,
|
|
314
329
|
appShellRef,
|
|
315
330
|
nonce,
|
|
331
|
+
hydration,
|
|
316
332
|
}: NavigationProviderProps): ReactNode {
|
|
317
333
|
// Track current payload for rendering (this triggers re-renders)
|
|
318
334
|
const [payload, setPayload] = useState(initialPayload);
|
|
319
335
|
// The location state of the entry `payload` renders (LocationStateContext).
|
|
320
336
|
// Only ever set next to setPayload, or for a commit that has no payload.
|
|
321
|
-
const [locationState, setLocationState] = useState(
|
|
322
|
-
eventController.getLocationState,
|
|
337
|
+
const [locationState, setLocationState] = useState(() =>
|
|
338
|
+
hydration ? undefined : eventController.getLocationState(),
|
|
323
339
|
);
|
|
340
|
+
// HydrationBarrierContext's value; see NavigationProviderProps.hydration.
|
|
341
|
+
const [hydrationBarrier, setHydrationBarrier] = useState(0);
|
|
342
|
+
const hydrationOpen = useRef(hydration !== undefined);
|
|
324
343
|
|
|
325
344
|
/**
|
|
326
345
|
* Navigate to a URL (delegates to bridge)
|
|
@@ -356,6 +375,8 @@ export function NavigationProvider({
|
|
|
356
375
|
navigate,
|
|
357
376
|
refresh,
|
|
358
377
|
defaultPrefetch: initialPayload.metadata.defaultPrefetch,
|
|
378
|
+
// Fixed for the document: read once, with the stable value.
|
|
379
|
+
origin: (eventController.getState().location as URL | undefined)?.origin,
|
|
359
380
|
} as NavigationStoreContextValue;
|
|
360
381
|
Object.defineProperty(value, "basename", {
|
|
361
382
|
configurable: true,
|
|
@@ -379,6 +400,32 @@ export function NavigationProvider({
|
|
|
379
400
|
return startConnectionWarmup();
|
|
380
401
|
}, [warmupEnabled]);
|
|
381
402
|
|
|
403
|
+
// The document has been revealed: one transition gives the entry's location
|
|
404
|
+
// state and moves the barrier, so React hydrates every boundary still
|
|
405
|
+
// dehydrated before it commits.
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (!hydration) return;
|
|
408
|
+
let cancelled = false;
|
|
409
|
+
void hydration.settled.then(() => {
|
|
410
|
+
if (cancelled || !hydrationOpen.current) return;
|
|
411
|
+
startTransition(() => {
|
|
412
|
+
setLocationState(eventController.getLocationState());
|
|
413
|
+
setHydrationBarrier((n) => n + 1);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
return () => {
|
|
417
|
+
cancelled = true;
|
|
418
|
+
};
|
|
419
|
+
}, []);
|
|
420
|
+
|
|
421
|
+
// After the barrier's commit no boundary under this provider is dehydrated:
|
|
422
|
+
// release the store, and its handle writes reach their readers.
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (hydrationBarrier === 0) return;
|
|
425
|
+
hydrationOpen.current = false;
|
|
426
|
+
eventController.releaseHydrationLock();
|
|
427
|
+
}, [hydrationBarrier]);
|
|
428
|
+
|
|
382
429
|
// A commit no payload follows (a shallow navigation, a commit that keeps
|
|
383
430
|
// every segment, merged server-set state): the tree on screen is already
|
|
384
431
|
// the entry's, so its state is taken with the commit's notification.
|
|
@@ -430,12 +477,23 @@ export function NavigationProvider({
|
|
|
430
477
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
431
478
|
useEffect(() => {
|
|
432
479
|
const unsubscribe = store.onUpdate((update) => {
|
|
480
|
+
if (process.env.NODE_ENV !== "production") auditTreeUpdate(update.root);
|
|
433
481
|
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
434
482
|
// after React commits this state update to the DOM.
|
|
435
483
|
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
436
484
|
// so server actions or error updates don't accidentally replay old scroll.
|
|
437
485
|
pendingScrollRef.current = update.scroll;
|
|
438
486
|
|
|
487
|
+
// Committed before the document finished hydrating: end the window
|
|
488
|
+
// first, so the page this update mounts reads the live store. A
|
|
489
|
+
// boundary of the document still dehydrated then hydrates against the
|
|
490
|
+
// live store; where that differs from its HTML, React reports the
|
|
491
|
+
// mismatch and renders the boundary on the client.
|
|
492
|
+
if (hydrationOpen.current) {
|
|
493
|
+
hydrationOpen.current = false;
|
|
494
|
+
eventController.releaseHydrationLock();
|
|
495
|
+
}
|
|
496
|
+
|
|
439
497
|
setPayload({
|
|
440
498
|
root: update.root,
|
|
441
499
|
metadata: update.metadata,
|
|
@@ -529,7 +587,9 @@ export function NavigationProvider({
|
|
|
529
587
|
return (
|
|
530
588
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
531
589
|
<LocationStateContext.Provider value={locationState}>
|
|
532
|
-
{
|
|
590
|
+
<HydrationBarrierContext.Provider value={hydrationBarrier}>
|
|
591
|
+
{content}
|
|
592
|
+
</HydrationBarrierContext.Provider>
|
|
533
593
|
</LocationStateContext.Provider>
|
|
534
594
|
</NavigationStoreContext.Provider>
|
|
535
595
|
);
|
|
@@ -57,6 +57,13 @@ export interface NavigationStoreContextValue {
|
|
|
57
57
|
|
|
58
58
|
/** Router default from this instance's initial payload. */
|
|
59
59
|
defaultPrefetch?: PrefetchStrategy;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The document's origin, fixed for its lifetime. Link compares absolute
|
|
63
|
+
* hrefs against it (data-external) and useLinkStatus resolves link targets
|
|
64
|
+
* with it.
|
|
65
|
+
*/
|
|
66
|
+
origin: string | undefined;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -79,3 +86,13 @@ export const NavigationStoreContext: Context<NavigationStoreContextValue | null>
|
|
|
79
86
|
*/
|
|
80
87
|
export const LocationStateContext: Context<LocationStateSnapshot> =
|
|
81
88
|
createContext<LocationStateSnapshot>(undefined);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read by no component. NavigationProvider changes its value in a transition so
|
|
92
|
+
* that React hydrates every boundary still dehydrated beneath it before that
|
|
93
|
+
* commit: React hydrates a dehydrated boundary first when a provider above it
|
|
94
|
+
* changes, and nothing re-renders for a value nobody reads. See
|
|
95
|
+
* NavigationProviderProps.hydration.
|
|
96
|
+
*/
|
|
97
|
+
export const HydrationBarrierContext: Context<number> =
|
|
98
|
+
createContext<number>(0);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useContext, useEffect
|
|
4
|
-
import { subscribeToNothing } from "./subscribe-to-nothing.js";
|
|
3
|
+
import { useContext, useEffect } from "react";
|
|
5
4
|
import { LocationStateContext } from "./context.js";
|
|
6
5
|
import { OptimisticLocationContext } from "../../client-urls/optimistic-location.js";
|
|
7
6
|
import {
|
|
@@ -19,15 +18,6 @@ export {
|
|
|
19
18
|
type LocationStateOptions,
|
|
20
19
|
} from "./location-state-shared.js";
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Server and hydration renders must match the SSR output (`undefined`).
|
|
24
|
-
* React then re-renders with `getSnapshot`, including a reader whose Suspense
|
|
25
|
-
* boundary hydrates after the root has set `data-hydrated`.
|
|
26
|
-
*/
|
|
27
|
-
function getServerSnapshot(): undefined {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
21
|
/**
|
|
32
22
|
* Hook to read the location state of the history entry on screen
|
|
33
23
|
*
|
|
@@ -73,16 +63,12 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
73
63
|
// Typed state: the slot under its key. Plain state: the `state` slot.
|
|
74
64
|
// Inside an optimistically rendered clientUrls() destination the provider
|
|
75
65
|
// still holds the entry being left: read the entry the navigation will push.
|
|
76
|
-
|
|
66
|
+
// While the document hydrates, the provider holds `undefined`, what SSR
|
|
67
|
+
// rendered, and gives the entry's state once every boundary has hydrated
|
|
68
|
+
// (NavigationProvider's hydration barrier, issue #992).
|
|
69
|
+
const state = (optimistic ? readableLocationState(optimistic) : entry)?.[
|
|
77
70
|
key || "state"
|
|
78
71
|
] as TState | undefined;
|
|
79
|
-
// useSyncExternalStore only for its server snapshot: no other hook tells a
|
|
80
|
-
// hydrating render from a client one.
|
|
81
|
-
const state = useSyncExternalStore<TState | undefined>(
|
|
82
|
-
subscribeToNothing,
|
|
83
|
-
() => shown,
|
|
84
|
-
getServerSnapshot,
|
|
85
|
-
);
|
|
86
72
|
|
|
87
73
|
// Flash: removed from history.state after paint, so a reload or a return to
|
|
88
74
|
// the entry does not show it again. The provider's snapshot keeps it, so it
|
|
@@ -149,7 +149,10 @@ export function useAction<T>(
|
|
|
149
149
|
if (!ctx) {
|
|
150
150
|
return selector ? selector(DEFAULT_ACTION_STATE) : DEFAULT_ACTION_STATE;
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
// The document renders no action in flight.
|
|
153
|
+
const state = ctx.eventController.getHydrationSnapshot()
|
|
154
|
+
? DEFAULT_ACTION_STATE
|
|
155
|
+
: ctx.eventController.getActionState(actionId);
|
|
153
156
|
return selector ? selector(state) : state;
|
|
154
157
|
});
|
|
155
158
|
const prevSelected = useRef(baseState);
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
useEffect,
|
|
7
7
|
useRef,
|
|
8
8
|
useOptimistic,
|
|
9
|
-
useSyncExternalStore,
|
|
10
9
|
startTransition,
|
|
11
10
|
} from "react";
|
|
12
11
|
import type { Handle } from "../../handle.js";
|
|
@@ -14,9 +13,6 @@ import { collectHandleData } from "../../handle.js";
|
|
|
14
13
|
import type { HandleData } from "../types.js";
|
|
15
14
|
import { NavigationStoreContext } from "./context.js";
|
|
16
15
|
import { shallowEqual } from "./shallow-equal.js";
|
|
17
|
-
import { subscribeToNothing } from "./subscribe-to-nothing.js";
|
|
18
|
-
|
|
19
|
-
const noHydrationState = (): undefined => undefined;
|
|
20
16
|
|
|
21
17
|
/**
|
|
22
18
|
* Hook to access collected handle data.
|
|
@@ -47,21 +43,6 @@ export function useHandle<T, A, S>(
|
|
|
47
43
|
): Rango.FlightSerialize<A> | S {
|
|
48
44
|
const ctx = useContext(NavigationStoreContext);
|
|
49
45
|
|
|
50
|
-
// useSyncExternalStore only for its server snapshot, as useLocationState
|
|
51
|
-
// (#992): no other hook tells a hydrating render from a client one. A
|
|
52
|
-
// hydrating render gets the handle state the document's HTML was rendered
|
|
53
|
-
// with (EventController.getHydrationHandleState), whenever its boundary
|
|
54
|
-
// hydrates; the mount effect below moves it on to the live state. That
|
|
55
|
-
// state is undefined while the live state is still it, and the client
|
|
56
|
-
// snapshot is always undefined: a reader that hydrated before anything
|
|
57
|
-
// arrived late is not rendered again, and to React the store never
|
|
58
|
-
// changes (issue #1035).
|
|
59
|
-
const hydrationState = useSyncExternalStore(
|
|
60
|
-
subscribeToNothing,
|
|
61
|
-
noHydrationState,
|
|
62
|
-
() => ctx?.eventController.getHydrationHandleState(),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
46
|
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
66
47
|
if (!ctx) {
|
|
67
48
|
const collected = collectHandleData(
|
|
@@ -72,7 +53,10 @@ export function useHandle<T, A, S>(
|
|
|
72
53
|
return selector ? selector(collected) : collected;
|
|
73
54
|
}
|
|
74
55
|
|
|
75
|
-
|
|
56
|
+
// Until every boundary has hydrated the store returns the state the
|
|
57
|
+
// document's HTML was rendered with (EventController.lockHydration), so a
|
|
58
|
+
// reader hydrating late reads what SSR rendered (issue #1035).
|
|
59
|
+
const state = ctx.eventController.getHandleState();
|
|
76
60
|
const collected = collectHandleData(
|
|
77
61
|
handle,
|
|
78
62
|
state.data,
|
|
@@ -82,14 +82,15 @@ export function useLinkStatus(): LinkStatus {
|
|
|
82
82
|
const linkTo = useContext(LinkContext);
|
|
83
83
|
const ctx = useContext(NavigationStoreContext);
|
|
84
84
|
|
|
85
|
-
const origin =
|
|
86
|
-
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
85
|
+
const origin = ctx?.origin ?? "http://localhost";
|
|
87
86
|
|
|
88
87
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
89
88
|
if (!ctx || linkTo === null) {
|
|
90
89
|
return false;
|
|
91
90
|
}
|
|
92
|
-
const state =
|
|
91
|
+
const state =
|
|
92
|
+
ctx.eventController.getHydrationSnapshot()?.state ??
|
|
93
|
+
ctx.eventController.getState();
|
|
93
94
|
return isPendingFor(linkTo, state.pendingUrl, origin);
|
|
94
95
|
});
|
|
95
96
|
|
|
@@ -47,7 +47,10 @@ export function useNavigation<T>(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
|
|
50
|
-
const publicState = toPublicState(
|
|
50
|
+
const publicState = toPublicState(
|
|
51
|
+
ctx.eventController.getHydrationSnapshot()?.state ??
|
|
52
|
+
ctx.eventController.getState(),
|
|
53
|
+
);
|
|
51
54
|
return selector ? selector(publicState) : publicState;
|
|
52
55
|
});
|
|
53
56
|
const prevState = useRef(baseValue);
|
|
@@ -49,7 +49,10 @@ export function useParams<T>(
|
|
|
49
49
|
const optimistic = useContext(OptimisticLocationContext);
|
|
50
50
|
|
|
51
51
|
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
52
|
-
const params = ctx
|
|
52
|
+
const params = ctx
|
|
53
|
+
? (ctx.eventController.getHydrationSnapshot()?.params ??
|
|
54
|
+
ctx.eventController.getParams())
|
|
55
|
+
: EMPTY_PARAMS;
|
|
53
56
|
return selector ? selector(params) : params;
|
|
54
57
|
});
|
|
55
58
|
|
|
@@ -26,7 +26,10 @@ export function usePathname(): string {
|
|
|
26
26
|
if (!ctx) {
|
|
27
27
|
return "/";
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const state =
|
|
30
|
+
ctx.eventController.getHydrationSnapshot()?.state ??
|
|
31
|
+
ctx.eventController.getState();
|
|
32
|
+
return (state.location as URL).pathname;
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
const prevPathname = useRef(pathname);
|
|
@@ -119,9 +119,11 @@ export function useSearchParams(): [ReadonlyURLSearchParams, SetSearchParams] {
|
|
|
119
119
|
// (no provider) seeds empty.
|
|
120
120
|
const [searchParams, setSearchParamsState] =
|
|
121
121
|
useState<ReadonlyURLSearchParams>(() => {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const state = ctx
|
|
123
|
+
? (ctx.eventController.getHydrationSnapshot()?.state ??
|
|
124
|
+
ctx.eventController.getState())
|
|
125
|
+
: undefined;
|
|
126
|
+
const location = state?.location as URL | undefined;
|
|
125
127
|
return new URLSearchParams(location?.searchParams);
|
|
126
128
|
});
|
|
127
129
|
|
|
@@ -64,16 +64,22 @@ export function useSegments<T>(
|
|
|
64
64
|
): T | SegmentsState {
|
|
65
65
|
const ctx = useContext(NavigationStoreContext);
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// is not
|
|
67
|
+
// The selector is applied when the store changes, never during render, as in
|
|
68
|
+
// useNavigation and useAction: the hook renders its own state only. A new
|
|
69
|
+
// selector identity is not a reactive input; it applies from the next store
|
|
70
|
+
// change.
|
|
71
|
+
const selectorRef = useRef(selector);
|
|
72
|
+
selectorRef.current = selector;
|
|
73
|
+
|
|
70
74
|
const [state, setState] = useState<T | SegmentsState>(() => {
|
|
71
75
|
if (!ctx) {
|
|
72
76
|
const fallbackLocation = new URL("/", "http://localhost");
|
|
73
77
|
const fallbackState = buildSegmentsState(fallbackLocation, []);
|
|
74
78
|
return selector ? selector(fallbackState) : fallbackState;
|
|
75
79
|
}
|
|
76
|
-
const location =
|
|
80
|
+
const location =
|
|
81
|
+
ctx.eventController.getHydrationSnapshot()?.state.location ??
|
|
82
|
+
ctx.eventController.getLocation();
|
|
77
83
|
const handleState = ctx.eventController.getHandleState();
|
|
78
84
|
const segmentsState = buildSegmentsState(
|
|
79
85
|
location as URL,
|
|
@@ -83,69 +89,45 @@ export function useSegments<T>(
|
|
|
83
89
|
});
|
|
84
90
|
|
|
85
91
|
const prevState = useRef(state);
|
|
86
|
-
const selectorRef = useRef(selector);
|
|
87
|
-
selectorRef.current = selector;
|
|
88
|
-
|
|
89
|
-
const prevSelectorIdentity = useRef(selector);
|
|
90
|
-
|
|
91
|
-
const segmentsCache = useRef<{
|
|
92
|
-
location: URL;
|
|
93
|
-
routeSegmentIds: string[];
|
|
94
|
-
state: SegmentsState;
|
|
95
|
-
} | null>(null);
|
|
96
|
-
|
|
97
|
-
function recompute(
|
|
98
|
-
sel: ((state: SegmentsState) => T) | undefined,
|
|
99
|
-
): T | SegmentsState {
|
|
100
|
-
const location = ctx!.eventController.getLocation();
|
|
101
|
-
const handleState = ctx!.eventController.getHandleState();
|
|
102
92
|
|
|
103
|
-
const cache = segmentsCache.current;
|
|
104
|
-
let segmentsState: SegmentsState;
|
|
105
|
-
if (
|
|
106
|
-
cache &&
|
|
107
|
-
cache.location === location &&
|
|
108
|
-
cache.routeSegmentIds === handleState.routeSegmentIds
|
|
109
|
-
) {
|
|
110
|
-
segmentsState = cache.state;
|
|
111
|
-
} else {
|
|
112
|
-
segmentsState = buildSegmentsState(
|
|
113
|
-
location as URL,
|
|
114
|
-
handleState.routeSegmentIds,
|
|
115
|
-
);
|
|
116
|
-
segmentsCache.current = {
|
|
117
|
-
location: location as URL,
|
|
118
|
-
routeSegmentIds: handleState.routeSegmentIds,
|
|
119
|
-
state: segmentsState,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return sel ? sel(segmentsState) : segmentsState;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (ctx && selector !== prevSelectorIdentity.current) {
|
|
126
|
-
prevSelectorIdentity.current = selector;
|
|
127
|
-
const nextSelected = recompute(selector);
|
|
128
|
-
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
129
|
-
prevState.current = nextSelected;
|
|
130
|
-
setState(nextSelected);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Subscribe to store changes. The eager block above handles selector
|
|
135
|
-
// changes and SSR drift, so no initial updateState() call is needed.
|
|
136
93
|
useEffect(() => {
|
|
137
94
|
if (!ctx) {
|
|
138
95
|
return;
|
|
139
96
|
}
|
|
140
97
|
|
|
98
|
+
// Reused while the location and route segments are unchanged, so a
|
|
99
|
+
// selector returning part of it keeps the same references.
|
|
100
|
+
let cache: {
|
|
101
|
+
location: URL;
|
|
102
|
+
routeSegmentIds: string[];
|
|
103
|
+
state: SegmentsState;
|
|
104
|
+
} | null = null;
|
|
105
|
+
|
|
141
106
|
const updateState = () => {
|
|
142
|
-
const
|
|
107
|
+
const location = ctx.eventController.getLocation() as URL;
|
|
108
|
+
const { routeSegmentIds } = ctx.eventController.getHandleState();
|
|
109
|
+
if (
|
|
110
|
+
!cache ||
|
|
111
|
+
cache.location !== location ||
|
|
112
|
+
cache.routeSegmentIds !== routeSegmentIds
|
|
113
|
+
) {
|
|
114
|
+
cache = {
|
|
115
|
+
location,
|
|
116
|
+
routeSegmentIds,
|
|
117
|
+
state: buildSegmentsState(location, routeSegmentIds),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const sel = selectorRef.current;
|
|
121
|
+
const nextSelected = sel ? sel(cache.state) : cache.state;
|
|
143
122
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
144
123
|
prevState.current = nextSelected;
|
|
145
124
|
setState(nextSelected);
|
|
146
125
|
}
|
|
147
126
|
};
|
|
148
127
|
|
|
128
|
+
// Catch up with a change between the seeding render and this effect.
|
|
129
|
+
updateState();
|
|
130
|
+
|
|
149
131
|
const unsubscribeNav = ctx.eventController.subscribe(updateState);
|
|
150
132
|
const unsubscribeHandles =
|
|
151
133
|
ctx.eventController.subscribeToHandles(updateState);
|