@rangojs/router 0.20.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 +39 -15
- package/dist/types/browser/event-controller.d.ts +49 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/navigation-transaction.d.ts +7 -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/rango-state.d.ts +3 -1
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +25 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- 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 +5 -6
- package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +59 -50
- package/dist/types/cache/index.d.ts +3 -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 +29 -4
- 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 +84 -24
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +18 -1
- 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/build-shell-capture.d.ts +8 -6
- 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 +15 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- 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 +32 -1
- package/dist/types/router/router-options.d.ts +39 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
- 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/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
- 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 +12 -6
- 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 +53 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +89 -46
- package/dist/types/server/request-context.d.ts +84 -9
- 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/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +3 -1
- package/dist/types/testing/index.d.ts +6 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +55 -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/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +15 -5
- 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/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +25 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1707 -931
- package/package.json +12 -3
- package/skills/cache-guide/SKILL.md +102 -37
- package/skills/caching/SKILL.md +56 -9
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +133 -1
- package/skills/comparison/references/framework-comparison.md +17 -13
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +25 -0
- package/skills/hooks/handle-and-actions.md +5 -0
- package/skills/hooks/navigation.md +38 -0
- package/skills/hooks/state.md +213 -11
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +31 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +25 -5
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +150 -48
- package/skills/prerender/SKILL.md +651 -10
- package/skills/rango/SKILL.md +9 -7
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +32 -27
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +167 -22
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +25 -10
- package/skills/vercel/SKILL.md +5 -3
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +119 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +104 -41
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +24 -12
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +58 -14
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +37 -43
- package/src/browser/react/NavigationProvider.tsx +85 -1
- package/src/browser/react/context.ts +30 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +28 -77
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +3 -0
- 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 +93 -31
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +43 -7
- 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 +48 -36
- package/src/cache/cf/cf-cache-store.ts +91 -66
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +118 -86
- package/src/cache/index.ts +5 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +52 -5
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +90 -24
- package/src/cache/vercel/vercel-cache-store.ts +68 -26
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +36 -0
- 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/build-shell-capture.ts +11 -9
- 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/manifest.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +239 -54
- package/src/router/navigation-snapshot.ts +25 -6
- 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/request-classification.ts +15 -12
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +35 -1
- package/src/router/router-options.ts +45 -5
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +110 -77
- 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-versions.ts +60 -0
- package/src/router.ts +107 -1
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +64 -7
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +65 -12
- package/src/rsc/shell-build-manifest.ts +17 -12
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +144 -45
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +12 -6
- 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 +130 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +357 -127
- package/src/server/request-context.ts +149 -12
- package/src/ssr/ssr-root.tsx +32 -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/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +4 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +293 -46
- package/src/testing/serve-shell-request.ts +262 -37
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +15 -5
- 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/build-versions.ts +1281 -0
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +33 -43
- package/src/vite/discovery/state.ts +36 -1
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +39 -6
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +96 -13
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/segment-content-promise.ts +0 -67
|
@@ -8,9 +8,12 @@ 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,
|
|
16
|
+
LocationStateContext,
|
|
14
17
|
NavigationStoreContext,
|
|
15
18
|
type NavigationStoreContextValue,
|
|
16
19
|
} from "./context.js";
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
deferredHandleNames,
|
|
37
40
|
resolveDeferredHandleValues,
|
|
38
41
|
} from "../../handles/deferred-resolution.js";
|
|
42
|
+
import { auditTreeUpdate } from "../../suspense-audit.js";
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Process handles from an async generator, updating the event controller
|
|
@@ -277,6 +281,18 @@ export interface NavigationProviderProps {
|
|
|
277
281
|
* it to seed a nonce so components calling useNonce() can be exercised.
|
|
278
282
|
*/
|
|
279
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> };
|
|
280
296
|
}
|
|
281
297
|
|
|
282
298
|
/**
|
|
@@ -312,9 +328,18 @@ export function NavigationProvider({
|
|
|
312
328
|
basename,
|
|
313
329
|
appShellRef,
|
|
314
330
|
nonce,
|
|
331
|
+
hydration,
|
|
315
332
|
}: NavigationProviderProps): ReactNode {
|
|
316
333
|
// Track current payload for rendering (this triggers re-renders)
|
|
317
334
|
const [payload, setPayload] = useState(initialPayload);
|
|
335
|
+
// The location state of the entry `payload` renders (LocationStateContext).
|
|
336
|
+
// Only ever set next to setPayload, or for a commit that has no payload.
|
|
337
|
+
const [locationState, setLocationState] = useState(() =>
|
|
338
|
+
hydration ? undefined : eventController.getLocationState(),
|
|
339
|
+
);
|
|
340
|
+
// HydrationBarrierContext's value; see NavigationProviderProps.hydration.
|
|
341
|
+
const [hydrationBarrier, setHydrationBarrier] = useState(0);
|
|
342
|
+
const hydrationOpen = useRef(hydration !== undefined);
|
|
318
343
|
|
|
319
344
|
/**
|
|
320
345
|
* Navigate to a URL (delegates to bridge)
|
|
@@ -350,6 +375,8 @@ export function NavigationProvider({
|
|
|
350
375
|
navigate,
|
|
351
376
|
refresh,
|
|
352
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,
|
|
353
380
|
} as NavigationStoreContextValue;
|
|
354
381
|
Object.defineProperty(value, "basename", {
|
|
355
382
|
configurable: true,
|
|
@@ -373,6 +400,45 @@ export function NavigationProvider({
|
|
|
373
400
|
return startConnectionWarmup();
|
|
374
401
|
}, [warmupEnabled]);
|
|
375
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
|
+
|
|
429
|
+
// A commit no payload follows (a shallow navigation, a commit that keeps
|
|
430
|
+
// every segment, merged server-set state): the tree on screen is already
|
|
431
|
+
// the entry's, so its state is taken with the commit's notification.
|
|
432
|
+
useEffect(
|
|
433
|
+
() =>
|
|
434
|
+
eventController.subscribe(() => {
|
|
435
|
+
if (eventController.takeTreelessLocationState()) {
|
|
436
|
+
setLocationState(eventController.getLocationState());
|
|
437
|
+
}
|
|
438
|
+
}),
|
|
439
|
+
[eventController],
|
|
440
|
+
);
|
|
441
|
+
|
|
376
442
|
// Cancel non-matching prefetches when navigation starts.
|
|
377
443
|
// Frees connections so the navigation fetch isn't competing with
|
|
378
444
|
// speculative prefetches. The prefetch matching the navigation target
|
|
@@ -411,16 +477,30 @@ export function NavigationProvider({
|
|
|
411
477
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
412
478
|
useEffect(() => {
|
|
413
479
|
const unsubscribe = store.onUpdate((update) => {
|
|
480
|
+
if (process.env.NODE_ENV !== "production") auditTreeUpdate(update.root);
|
|
414
481
|
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
415
482
|
// after React commits this state update to the DOM.
|
|
416
483
|
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
417
484
|
// so server actions or error updates don't accidentally replay old scroll.
|
|
418
485
|
pendingScrollRef.current = update.scroll;
|
|
419
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
|
+
|
|
420
497
|
setPayload({
|
|
421
498
|
root: update.root,
|
|
422
499
|
metadata: update.metadata,
|
|
423
500
|
});
|
|
501
|
+
// The state of the entry this payload renders, as its commit recorded
|
|
502
|
+
// it: one update, so one lane, so one React commit with the tree.
|
|
503
|
+
setLocationState(eventController.getLocationState());
|
|
424
504
|
|
|
425
505
|
// Update route params. Only reset when the server actually sends a params
|
|
426
506
|
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
@@ -506,7 +586,11 @@ export function NavigationProvider({
|
|
|
506
586
|
|
|
507
587
|
return (
|
|
508
588
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
509
|
-
{
|
|
589
|
+
<LocationStateContext.Provider value={locationState}>
|
|
590
|
+
<HydrationBarrierContext.Provider value={hydrationBarrier}>
|
|
591
|
+
{content}
|
|
592
|
+
</HydrationBarrierContext.Provider>
|
|
593
|
+
</LocationStateContext.Provider>
|
|
510
594
|
</NavigationStoreContext.Provider>
|
|
511
595
|
);
|
|
512
596
|
}
|
|
@@ -4,6 +4,7 @@ import { createContext, type Context } from "react";
|
|
|
4
4
|
import type { NavigationStore, NavigateOptionsInternal } from "../types.js";
|
|
5
5
|
import type { EventController } from "../event-controller.js";
|
|
6
6
|
import type { PrefetchStrategy } from "../../router/prefetch-default.js";
|
|
7
|
+
import type { LocationStateSnapshot } from "./location-state-shared.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Navigation context value provided by NavigationProvider
|
|
@@ -56,6 +57,13 @@ export interface NavigationStoreContextValue {
|
|
|
56
57
|
|
|
57
58
|
/** Router default from this instance's initial payload. */
|
|
58
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;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -66,3 +74,25 @@ export interface NavigationStoreContextValue {
|
|
|
66
74
|
*/
|
|
67
75
|
export const NavigationStoreContext: Context<NavigationStoreContextValue | null> =
|
|
68
76
|
createContext<NavigationStoreContextValue | null>(null);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The location state of the history entry whose tree is on screen, for
|
|
80
|
+
* useLocationState. NavigationProvider holds it as React state next to the
|
|
81
|
+
* payload and sets both in one update, so a render sees an entry's state
|
|
82
|
+
* exactly when it sees that entry's tree: a transition React holds renders
|
|
83
|
+
* the destination's value, an urgent render of the content still on screen
|
|
84
|
+
* (a reader mounting there included) the value of the entry being left
|
|
85
|
+
* (#1029). Undefined on the server and outside a provider.
|
|
86
|
+
*/
|
|
87
|
+
export const LocationStateContext: Context<LocationStateSnapshot> =
|
|
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);
|
|
@@ -11,12 +11,177 @@ export interface LocationStateEntry {
|
|
|
11
11
|
readonly __rsc_ls_lazy?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Prefix of every typed location-state slot in `history.state`. */
|
|
15
|
+
export const LOCATION_STATE_KEY_PREFIX: string = "__rsc_ls_";
|
|
16
|
+
|
|
17
|
+
export function isLocationStateKey(key: string): boolean {
|
|
18
|
+
return key.startsWith(LOCATION_STATE_KEY_PREFIX);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `clearOnReload` is part of a slot's KEY (`<key>~r`); the value is always
|
|
23
|
+
* stored raw. Slots outlive deploys, so nothing that reads one may depend on a
|
|
24
|
+
* stored format: a definition without the option reads the plain key and never
|
|
25
|
+
* sees such a slot.
|
|
26
|
+
*
|
|
27
|
+
* An injected key ends in `#<ExportName>` (expose-id-utils.ts makeStubId; only
|
|
28
|
+
* identifier exports are keyed) and "~" cannot occur in an identifier, so no
|
|
29
|
+
* file or export name produces a suffix. withLocationStateKey rejects it.
|
|
30
|
+
*/
|
|
31
|
+
export const LOCATION_STATE_KEY_SUFFIX_SEPARATOR: string = "~";
|
|
32
|
+
export const CLEAR_ON_RELOAD_KEY_SUFFIX: string = "~r";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A history entry records the app version its location state (typed slots and
|
|
36
|
+
* plain `state`) was written under, in this one field; a read returns only
|
|
37
|
+
* state recorded under the version this document loaded with. Entries outlive
|
|
38
|
+
* deploys, and state an older release stored would otherwise reach code that
|
|
39
|
+
* no longer expects its shape. An entry without the field counts as another
|
|
40
|
+
* version.
|
|
41
|
+
*
|
|
42
|
+
* Not a slot key (no LOCATION_STATE_KEY_PREFIX): hasLocationState must not
|
|
43
|
+
* count it.
|
|
44
|
+
*/
|
|
45
|
+
const LOCATION_STATE_VERSION_KEY = "__rsc_lsv";
|
|
46
|
+
|
|
47
|
+
let documentVersion: string | undefined;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @internal The version the document loaded under (rsc-router.tsx
|
|
51
|
+
* initBrowserApp). Deliberately not getAppVersion(): a dev HMR bump moves that
|
|
52
|
+
* one inside a running session (bridge.updateVersion), and the state that
|
|
53
|
+
* session wrote must stay readable until the next document load.
|
|
54
|
+
*/
|
|
55
|
+
export function setLocationStateVersion(version: string | undefined): void {
|
|
56
|
+
documentVersion = version;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The state of a `{ state }` snapshot when its location state may be read:
|
|
61
|
+
* an object recorded under the client's version. Without a snapshot, the
|
|
62
|
+
* current entry (`window.history` is a `{ state }`), and nothing on the
|
|
63
|
+
* server.
|
|
64
|
+
*/
|
|
65
|
+
export function readableLocationState(
|
|
66
|
+
location: { readonly state: unknown } | undefined = typeof window ===
|
|
67
|
+
"undefined"
|
|
68
|
+
? undefined
|
|
69
|
+
: window.history,
|
|
70
|
+
): Record<string, unknown> | undefined {
|
|
71
|
+
const state = location?.state;
|
|
72
|
+
return state !== null &&
|
|
73
|
+
typeof state === "object" &&
|
|
74
|
+
(state as Record<string, unknown>)[LOCATION_STATE_VERSION_KEY] ===
|
|
75
|
+
documentVersion
|
|
76
|
+
? (state as Record<string, unknown>)
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One history entry's location state as useLocationState readers receive it:
|
|
82
|
+
* its typed slots and plain `state`, or undefined when it holds none.
|
|
83
|
+
*/
|
|
84
|
+
export type LocationStateSnapshot =
|
|
85
|
+
| Readonly<Record<string, unknown>>
|
|
86
|
+
| undefined;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The snapshot of `entryState`: the state object a commit pushes, restores or
|
|
90
|
+
* merges into its history entry. Only state recorded under the client's
|
|
91
|
+
* version is in it (readableLocationState).
|
|
92
|
+
*
|
|
93
|
+
* The snapshot is a React context value, so identity decides who re-renders.
|
|
94
|
+
* It is `previous` itself when every slot holds the value `previous` holds:
|
|
95
|
+
* an entry without state after another, a primitive carried over, or the
|
|
96
|
+
* object a reader got from `previous` passed back by the navigation. Any
|
|
97
|
+
* other value is new, equal content included: a link clicked twice announces
|
|
98
|
+
* its state twice (a flash message, a scroll target). A new value is a
|
|
99
|
+
* structured clone, as history.state holds it, so a reader never shares an
|
|
100
|
+
* object with the caller of the navigation.
|
|
101
|
+
*/
|
|
102
|
+
export function locationStateSnapshot(
|
|
103
|
+
entryState: unknown,
|
|
104
|
+
previous?: LocationStateSnapshot,
|
|
105
|
+
): LocationStateSnapshot {
|
|
106
|
+
const state = readableLocationState({ state: entryState });
|
|
107
|
+
let next: Record<string, unknown> | undefined;
|
|
108
|
+
let kept = 0;
|
|
109
|
+
for (const key in state) {
|
|
110
|
+
if (key !== "state" && !isLocationStateKey(key)) continue;
|
|
111
|
+
next ??= {};
|
|
112
|
+
const value = state[key];
|
|
113
|
+
if (
|
|
114
|
+
previous &&
|
|
115
|
+
Object.hasOwn(previous, key) &&
|
|
116
|
+
Object.is(previous[key], value)
|
|
117
|
+
) {
|
|
118
|
+
next[key] = value;
|
|
119
|
+
kept++;
|
|
120
|
+
} else {
|
|
121
|
+
next[key] = structuredClone(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return previous &&
|
|
125
|
+
next &&
|
|
126
|
+
kept === Object.keys(next).length &&
|
|
127
|
+
kept === Object.keys(previous).length
|
|
128
|
+
? previous
|
|
129
|
+
: next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Record the client's version on a state object that carries location state
|
|
134
|
+
* and no record yet. A client without a version (a renderRoute tree) writes
|
|
135
|
+
* none.
|
|
136
|
+
*/
|
|
137
|
+
export function stampLocationState(state: Record<string, unknown>): void {
|
|
138
|
+
if (documentVersion !== undefined) {
|
|
139
|
+
state[LOCATION_STATE_VERSION_KEY] = documentVersion;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `entryState` plus `additions`, recorded under the client's version. The
|
|
145
|
+
* record is per entry, so location state another version left there is dropped
|
|
146
|
+
* first, with its record: under the new one it would be readable again. Router
|
|
147
|
+
* bookkeeping (idx, scroll key, intercept) is kept.
|
|
148
|
+
*/
|
|
149
|
+
export function addLocationState(
|
|
150
|
+
entryState: unknown,
|
|
151
|
+
additions?: Record<string, unknown>,
|
|
152
|
+
): Record<string, unknown> {
|
|
153
|
+
// history.state may be a primitive if non-Rango code stored one; spreading a
|
|
154
|
+
// string would yield indexed char keys.
|
|
155
|
+
const next: Record<string, unknown> = {
|
|
156
|
+
...(typeof entryState === "object" ? entryState : null),
|
|
157
|
+
};
|
|
158
|
+
if (next[LOCATION_STATE_VERSION_KEY] !== documentVersion) {
|
|
159
|
+
for (const key in next) {
|
|
160
|
+
if (
|
|
161
|
+
key === "state" ||
|
|
162
|
+
key === LOCATION_STATE_VERSION_KEY ||
|
|
163
|
+
isLocationStateKey(key)
|
|
164
|
+
) {
|
|
165
|
+
delete next[key];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
stampLocationState(next);
|
|
169
|
+
}
|
|
170
|
+
return Object.assign(next, additions);
|
|
171
|
+
}
|
|
172
|
+
|
|
14
173
|
/**
|
|
15
174
|
* Options for createLocationState
|
|
16
175
|
*/
|
|
17
176
|
export interface LocationStateOptions {
|
|
18
177
|
/** When true, the state is cleared from history after first read (flash message pattern) */
|
|
19
178
|
flash?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* When true, the state does not survive a document load of its entry (a
|
|
181
|
+
* refresh, a back/forward that loads the document): the server rendered
|
|
182
|
+
* without it. Not together with `flash`.
|
|
183
|
+
*/
|
|
184
|
+
clearOnReload?: boolean;
|
|
20
185
|
}
|
|
21
186
|
|
|
22
187
|
type LocationStateUnsafeFn = (...args: never[]) => unknown;
|
|
@@ -236,13 +401,16 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
236
401
|
(...args: TArgs): LocationStateEntry;
|
|
237
402
|
/**
|
|
238
403
|
* Injected by Vite plugin - do not set manually. Unit tests without the
|
|
239
|
-
* plugin use withLocationStateKey() from @rangojs/router/testing.
|
|
404
|
+
* plugin use withLocationStateKey() from @rangojs/router/testing. Reads back
|
|
405
|
+
* as the slot's key in `history.state`: the injected key plus the
|
|
406
|
+
* `clearOnReload` suffix.
|
|
240
407
|
*/
|
|
241
408
|
__rsc_ls_key: string;
|
|
242
409
|
/** Whether this state auto-clears after first read */
|
|
243
410
|
readonly __rsc_ls_flash: boolean;
|
|
244
411
|
/**
|
|
245
|
-
* Read the current value from history.state.
|
|
412
|
+
* Read the current value from history.state. State the entry holds from
|
|
413
|
+
* another version of the app (an older build) reads as undefined.
|
|
246
414
|
*
|
|
247
415
|
* Returns undefined during SSR (no `window`). To stay hydration-safe, do
|
|
248
416
|
* NOT call read() inline during the initial render — the server returns
|
|
@@ -263,13 +431,14 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
263
431
|
/**
|
|
264
432
|
* Statically write the value into the current history entry under this
|
|
265
433
|
* definition's key, preserving any other keys already on history.state
|
|
266
|
-
* (e.g. router bookkeeping, other LocationState slots).
|
|
434
|
+
* (e.g. router bookkeeping, other LocationState slots). Location state
|
|
435
|
+
* another version of the app left on the entry is dropped.
|
|
267
436
|
*
|
|
268
|
-
* This is the non-reactive counterpart to read(): it
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
437
|
+
* This is the non-reactive counterpart to read(): it changes the history
|
|
438
|
+
* entry and nothing a useLocationState() reader shows, whether the reader
|
|
439
|
+
* is mounted now or mounts later. read() returns the value at once; a
|
|
440
|
+
* reader gets it when the entry is next restored: back/forward to it, or
|
|
441
|
+
* (unless the definition sets `clearOnReload`) a hard refresh of it.
|
|
273
442
|
*
|
|
274
443
|
* Client-only: throws when called on the server (no history available).
|
|
275
444
|
*/
|
|
@@ -279,8 +448,8 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
279
448
|
* leaving any other keys on history.state untouched. Idempotent: removing
|
|
280
449
|
* a slot that isn't present is a no-op.
|
|
281
450
|
*
|
|
282
|
-
* Same non-reactive semantics as write():
|
|
283
|
-
*
|
|
451
|
+
* Same non-reactive semantics as write(): useLocationState() readers keep
|
|
452
|
+
* the value until the entry is next restored.
|
|
284
453
|
*
|
|
285
454
|
* Client-only: throws when called on the server (no history available).
|
|
286
455
|
*/
|
|
@@ -321,9 +490,8 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
321
490
|
* const snap = ProductState.read();
|
|
322
491
|
*
|
|
323
492
|
* // Static write to current history entry (non-reactive, client-side only).
|
|
324
|
-
* // Survives back/forward and hard refresh; useLocationState() readers
|
|
325
|
-
* //
|
|
326
|
-
* // fresh mount.
|
|
493
|
+
* // Survives back/forward and hard refresh; useLocationState() readers see
|
|
494
|
+
* // the new value when the entry is next restored. Pair with .read().
|
|
327
495
|
* ProductState.write({ name: "Widget", price: 9.99 });
|
|
328
496
|
*
|
|
329
497
|
* // Manually clear the slot (non-reactive, client-side only).
|
|
@@ -357,6 +525,19 @@ export function createLocationState<TState>(
|
|
|
357
525
|
TState
|
|
358
526
|
> {
|
|
359
527
|
const flash = options?.flash ?? false;
|
|
528
|
+
const clearOnReload = options?.clearOnReload ?? false;
|
|
529
|
+
// Empty without `clearOnReload`: the injected key is then the storage key,
|
|
530
|
+
// as it was before the option existed.
|
|
531
|
+
const keySuffix = clearOnReload ? CLEAR_ON_RELOAD_KEY_SUFFIX : "";
|
|
532
|
+
// Flash state ends at its first read, so the pair could only drop a message
|
|
533
|
+
// nobody has seen. Folded out of production, like the missing-key check.
|
|
534
|
+
if (process.env.NODE_ENV !== "production" && flash && clearOnReload) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
"[rango] createLocationState: `flash` and `clearOnReload` cannot be combined. " +
|
|
537
|
+
"Flash state is removed at its first read, so `clearOnReload` could only " +
|
|
538
|
+
"drop a flash message that has not been shown yet.",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
360
541
|
let _key: string | undefined;
|
|
361
542
|
|
|
362
543
|
// Dev and test throw; production folds the check away (the plugin always
|
|
@@ -395,7 +576,7 @@ export function createLocationState<TState>(
|
|
|
395
576
|
Object.defineProperty(fn, "__rsc_ls_key", {
|
|
396
577
|
get: () => getKey(),
|
|
397
578
|
set: (k: string) => {
|
|
398
|
-
_key = k;
|
|
579
|
+
_key = k && k + keySuffix;
|
|
399
580
|
},
|
|
400
581
|
enumerable: true,
|
|
401
582
|
configurable: true,
|
|
@@ -407,16 +588,8 @@ export function createLocationState<TState>(
|
|
|
407
588
|
});
|
|
408
589
|
|
|
409
590
|
Object.defineProperty(fn, "read", {
|
|
410
|
-
value: (location?: { readonly state: unknown }): TState | undefined =>
|
|
411
|
-
|
|
412
|
-
const state = location.state;
|
|
413
|
-
return state !== null && typeof state === "object"
|
|
414
|
-
? ((state as Record<string, unknown>)[getKey()] as TState | undefined)
|
|
415
|
-
: undefined;
|
|
416
|
-
}
|
|
417
|
-
if (typeof window === "undefined") return undefined;
|
|
418
|
-
return window.history.state?.[getKey()] as TState | undefined;
|
|
419
|
-
},
|
|
591
|
+
value: (location?: { readonly state: unknown }): TState | undefined =>
|
|
592
|
+
readableLocationState(location)?.[getKey()] as TState | undefined,
|
|
420
593
|
enumerable: true,
|
|
421
594
|
});
|
|
422
595
|
|
|
@@ -428,16 +601,9 @@ export function createLocationState<TState>(
|
|
|
428
601
|
"It mutates window.history.state and cannot run on the server.",
|
|
429
602
|
);
|
|
430
603
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// catches null/undefined, so spreading a primitive would yield indexed
|
|
435
|
-
// char/no keys and corrupt history.state. Coerce any non-object to a fresh
|
|
436
|
-
// dict — mirrors the delete() guard.
|
|
437
|
-
const existing = window.history.state;
|
|
438
|
-
const current =
|
|
439
|
-
existing !== null && typeof existing === "object" ? existing : {};
|
|
440
|
-
replaceCurrentHistoryState({ ...current, [key]: value });
|
|
604
|
+
replaceCurrentHistoryState(
|
|
605
|
+
addLocationState(window.history.state, { [getKey()]: value }),
|
|
606
|
+
);
|
|
441
607
|
},
|
|
442
608
|
enumerable: true,
|
|
443
609
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useContext, useEffect } from "react";
|
|
4
|
+
import { LocationStateContext } from "./context.js";
|
|
5
|
+
import { OptimisticLocationContext } from "../../client-urls/optimistic-location.js";
|
|
4
6
|
import {
|
|
7
|
+
readableLocationState,
|
|
5
8
|
replaceCurrentHistoryState,
|
|
6
9
|
type LocationStateDefinition,
|
|
7
10
|
} from "./location-state-shared.js";
|
|
@@ -15,29 +18,16 @@ export {
|
|
|
15
18
|
type LocationStateOptions,
|
|
16
19
|
} from "./location-state-shared.js";
|
|
17
20
|
|
|
18
|
-
function readLocationStateValue<TState>(
|
|
19
|
-
key: string | undefined,
|
|
20
|
-
): TState | undefined {
|
|
21
|
-
if (typeof window === "undefined") return undefined;
|
|
22
|
-
if (key) {
|
|
23
|
-
return window.history.state?.[key] as TState | undefined;
|
|
24
|
-
}
|
|
25
|
-
// Plain state: stored under history.state.state
|
|
26
|
-
return window.history.state?.state as TState | undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function hasHydrated(): boolean {
|
|
30
|
-
return (
|
|
31
|
-
typeof document !== "undefined" &&
|
|
32
|
-
document.documentElement.hasAttribute("data-hydrated")
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
21
|
/**
|
|
37
|
-
* Hook to read location state
|
|
22
|
+
* Hook to read the location state of the history entry on screen
|
|
23
|
+
*
|
|
24
|
+
* A reader sees an entry's state together with that entry's tree: on a
|
|
25
|
+
* navigation the value changes in the React commit that brings the
|
|
26
|
+
* destination, so content a pending navigation keeps on screen keeps the
|
|
27
|
+
* state of the entry being left, for a reader that mounts there too (#1029).
|
|
38
28
|
*
|
|
39
29
|
* Behavior depends on the definition:
|
|
40
|
-
* - Normal state: persists across navigations
|
|
30
|
+
* - Normal state: persists across navigations and back/forward
|
|
41
31
|
* - Flash state (created with { flash: true }): read once, cleared after paint
|
|
42
32
|
*
|
|
43
33
|
* Overloaded:
|
|
@@ -67,69 +57,30 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
67
57
|
): TState | undefined {
|
|
68
58
|
const key = definition?.__rsc_ls_key;
|
|
69
59
|
const isFlash = definition?.__rsc_ls_flash ?? false;
|
|
60
|
+
const entry = useContext(LocationStateContext);
|
|
61
|
+
const optimistic = useContext(OptimisticLocationContext);
|
|
70
62
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!hasHydrated()) {
|
|
81
|
-
initialReadDeferredRef.current = true;
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
return readLocationStateValue<TState>(key);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Subscribe to popstate and programmatic state changes
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const handlePopstate = () => {
|
|
90
|
-
setState(readLocationStateValue<TState>(key));
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Handle programmatic state changes (same-page navigation with
|
|
94
|
-
// ctx.setLocationState where components don't remount)
|
|
95
|
-
const handleLocationState = () => {
|
|
96
|
-
if (key) {
|
|
97
|
-
const val = readLocationStateValue<TState>(key);
|
|
98
|
-
if (isFlash) {
|
|
99
|
-
// For flash state, only update if there's a new value
|
|
100
|
-
if (val !== undefined) {
|
|
101
|
-
setState(val);
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
setState(val);
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
setState(readLocationStateValue<TState>(key));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
if (initialReadDeferredRef.current) {
|
|
112
|
-
initialReadDeferredRef.current = false;
|
|
113
|
-
setState(readLocationStateValue<TState>(key));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
window.addEventListener("popstate", handlePopstate);
|
|
117
|
-
window.addEventListener("__rsc_locationstate", handleLocationState);
|
|
118
|
-
return () => {
|
|
119
|
-
window.removeEventListener("popstate", handlePopstate);
|
|
120
|
-
window.removeEventListener("__rsc_locationstate", handleLocationState);
|
|
121
|
-
};
|
|
122
|
-
}, [key, isFlash]);
|
|
63
|
+
// Typed state: the slot under its key. Plain state: the `state` slot.
|
|
64
|
+
// Inside an optimistically rendered clientUrls() destination the provider
|
|
65
|
+
// still holds the entry being left: read the entry the navigation will push.
|
|
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)?.[
|
|
70
|
+
key || "state"
|
|
71
|
+
] as TState | undefined;
|
|
123
72
|
|
|
124
|
-
// Flash:
|
|
125
|
-
//
|
|
73
|
+
// Flash: removed from history.state after paint, so a reload or a return to
|
|
74
|
+
// the entry does not show it again. The provider's snapshot keeps it, so it
|
|
75
|
+
// stays on screen until the next commit of the entry's state. Not while the
|
|
76
|
+
// value is the optimistic entry's: history.state is another entry's.
|
|
126
77
|
useEffect(() => {
|
|
127
|
-
if (isFlash && key && state !== undefined) {
|
|
78
|
+
if (isFlash && key && !optimistic && state !== undefined) {
|
|
128
79
|
const cleaned = { ...window.history.state };
|
|
129
80
|
delete cleaned[key];
|
|
130
81
|
replaceCurrentHistoryState(cleaned);
|
|
131
82
|
}
|
|
132
|
-
}, [isFlash, key, state]);
|
|
83
|
+
}, [isFlash, key, state, optimistic]);
|
|
133
84
|
|
|
134
85
|
return state;
|
|
135
86
|
}
|
|
@@ -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);
|
|
@@ -53,6 +53,9 @@ export function useHandle<T, A, S>(
|
|
|
53
53
|
return selector ? selector(collected) : collected;
|
|
54
54
|
}
|
|
55
55
|
|
|
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).
|
|
56
59
|
const state = ctx.eventController.getHandleState();
|
|
57
60
|
const collected = collectHandleData(
|
|
58
61
|
handle,
|