@rangojs/router 0.20.0 → 0.21.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 +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -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/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- 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 +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- 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 +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- 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 +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- 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 +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- 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/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 +1537 -444
- package/package.json +1 -2
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +30 -26
- 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 +63 -14
- 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 +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- 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/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/ssr-root.tsx +16 -4
- 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.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -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 +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- 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 +31 -5
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +82 -13
- 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
|
@@ -128,7 +128,7 @@ it("asserts the server-rendered host content", async () => {
|
|
|
128
128
|
- A client boundary's props come back as REAL JS values after deserialization (a `Date` is a `Date`, not a `$D...` encoding) — but there is NO hydration and NO interaction; boundaries are inert placeholders carrying props.
|
|
129
129
|
- Server COMPONENTS do not survive Flight as identities (they are executed during serialization), so `findElements` matches the host elements they PRODUCED, not the component function. Client islands keep identity — use `findClientBoundaries` for those.
|
|
130
130
|
- `findClientBoundaries` finds islands (`I` rows); `findElements` finds host elements. A `testId` on an island matches with `findClientBoundaries`; a `testId` on a host element matches with `findElements`. Use `textContent(node)` in place of `JSON.stringify(tree).toContain`.
|
|
131
|
-
- A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration re-tests React more than your app and misses
|
|
131
|
+
- A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration of a Flight tree re-tests React more than your app and misses the divergence that needs the real document (streaming order, client-reference identity, the browser's HTML parser). Test interaction at e2e. A CLIENT component's own server/client divergence is unit-reachable: `renderRoute` with `hydrate: true` ([`./client-components.md`](./client-components.md#hydration)).
|
|
132
132
|
|
|
133
133
|
## See also
|
|
134
134
|
|
|
@@ -113,7 +113,7 @@ and client helpers.
|
|
|
113
113
|
CLI flags: the default mode fails (and writes nothing) when an `include()` cannot
|
|
114
114
|
be resolved statically (factory functions, dynamic expressions); `--static`
|
|
115
115
|
accepts partial output with warnings; `--runtime` uses Vite-based discovery for
|
|
116
|
-
full coverage (needs `vite
|
|
116
|
+
full coverage (needs `vite`, optional
|
|
117
117
|
`--config <path>`). Passing a directory scans it for router and `urls()` files.
|
|
118
118
|
|
|
119
119
|
### Named Routes, `$$routeNames`, And `router.routeMap`
|
|
@@ -485,11 +485,22 @@ the current time and expect freshness -- those are frozen at cache-write. The
|
|
|
485
485
|
`cookies()`/`headers()` read guard applies to the cached function body, NOT to an
|
|
486
486
|
inline action's body.
|
|
487
487
|
|
|
488
|
-
Deploy note: captured values are encrypted with a
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
488
|
+
Deploy note: captured values are encrypted with a key that is generated per
|
|
489
|
+
build by default. Set a stable one in the plugin options:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY });
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The value is base64-encoded 32 bytes (`openssl rand -base64 32`), never a
|
|
496
|
+
literal, and is validated when `rango()` is called. `undefined` falls back to the
|
|
497
|
+
`RANGO_ENCRYPTION_KEY` environment variable, then to a key generated per build.
|
|
498
|
+
The key is part of the cache version of every router whose server code encrypts
|
|
499
|
+
inline server-action bound arguments, so without a stable key those routers get a
|
|
500
|
+
new version (and a cleared cache) on every build, and the build prints one note
|
|
501
|
+
when that applies. With a stable key, actions embedded in entries written by a
|
|
502
|
+
previous deploy still decrypt. A router that renders no inline action with bound
|
|
503
|
+
arguments keeps its cache without a stable key. See `/cache-guide`.
|
|
493
504
|
|
|
494
505
|
## Vite Transform
|
|
495
506
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -98,9 +98,7 @@ const memoryStore = new MemorySegmentCacheStore({ defaults });
|
|
|
98
98
|
// passes, since the tag markers are regional and only expireTag is global. A
|
|
99
99
|
// platform expireTag issued outside rango writes no marker, so every memo
|
|
100
100
|
// serves the shell until its window passes.
|
|
101
|
-
const runtimeCache = process.env.VERCEL
|
|
102
|
-
? getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })
|
|
103
|
-
: undefined;
|
|
101
|
+
const runtimeCache = process.env.VERCEL ? getCache() : undefined;
|
|
104
102
|
|
|
105
103
|
function resolveCache() {
|
|
106
104
|
if (runtimeCache) {
|
|
@@ -120,6 +118,8 @@ export const router = createRouter({ cache: resolveCache }).routes(/* ... */);
|
|
|
120
118
|
|
|
121
119
|
The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`; off-platform `ctx` is `undefined`. Cache semantics and tag invalidation: `/caching`, `/cache-guide`.
|
|
122
120
|
|
|
121
|
+
`getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. `version` on the store (or on `createRouter`) replaces both versions. To clear the cache on every deploy, set `version` to a per-deploy value or keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace or `version`.
|
|
122
|
+
|
|
123
123
|
## Host routers (multi-app)
|
|
124
124
|
|
|
125
125
|
A multi-app host router deploys as a **single function** running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model). Two requirements:
|
package/src/bin/rango.ts
CHANGED
|
@@ -76,7 +76,7 @@ if (command === "generate") {
|
|
|
76
76
|
Modes:
|
|
77
77
|
(default) Static parser with error on unresolvable includes
|
|
78
78
|
--runtime Vite-based runtime discovery (100% coverage)
|
|
79
|
-
Requires vite
|
|
79
|
+
Requires vite
|
|
80
80
|
--static Static parser, accept partial output with warnings
|
|
81
81
|
|
|
82
82
|
Options:
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
filterRouteSegmentIds,
|
|
16
16
|
} from "./react/filter-segment-order.js";
|
|
17
17
|
import { notifyListeners } from "./notify-listeners.js";
|
|
18
|
+
import { cloneHandleData } from "./navigation-store.js";
|
|
19
|
+
import {
|
|
20
|
+
locationStateSnapshot,
|
|
21
|
+
type LocationStateSnapshot,
|
|
22
|
+
} from "./react/location-state-shared.js";
|
|
18
23
|
|
|
19
24
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
20
25
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -224,7 +229,8 @@ export interface EventController {
|
|
|
224
229
|
getActionState(actionId: string): TrackedActionState;
|
|
225
230
|
getLocation(): NavigationLocation;
|
|
226
231
|
|
|
227
|
-
// Location
|
|
232
|
+
// Location of a commit no transaction completes: a shallow navigation, a
|
|
233
|
+
// back/forward restored from the history cache, a back/forward that failed.
|
|
228
234
|
setLocation(location: NavigationLocation): void;
|
|
229
235
|
|
|
230
236
|
// Subscriptions
|
|
@@ -256,6 +262,23 @@ export interface EventController {
|
|
|
256
262
|
resolvedIds?: string[],
|
|
257
263
|
): void;
|
|
258
264
|
getHandleState(): HandleState;
|
|
265
|
+
/**
|
|
266
|
+
* Keep the current handle state as the one the document's HTML was rendered
|
|
267
|
+
* with. initBrowserApp calls it before hydrateRoot, once the document's
|
|
268
|
+
* handle snapshot is applied.
|
|
269
|
+
*/
|
|
270
|
+
freezeHydrationHandleState(): void;
|
|
271
|
+
/**
|
|
272
|
+
* The handle state the document's HTML was rendered with, once the live
|
|
273
|
+
* state has moved on from it; undefined while the live state is still that
|
|
274
|
+
* state (nothing changed since the freeze, or nothing was frozen: a tree
|
|
275
|
+
* that was mounted, an SSR pass). A render React is hydrating reads it
|
|
276
|
+
* (useHandle), whenever its boundary hydrates: the late handle channel
|
|
277
|
+
* (rsc-router.tsx) waits for the root to hydrate, not for a `<Suspense>`
|
|
278
|
+
* boundary that hydrates after it, and by then the live state can hold
|
|
279
|
+
* values that boundary's HTML does not (issue #1035).
|
|
280
|
+
*/
|
|
281
|
+
getHydrationHandleState(): HandleState | undefined;
|
|
259
282
|
/**
|
|
260
283
|
* Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
|
|
261
284
|
* leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
|
|
@@ -269,6 +292,21 @@ export interface EventController {
|
|
|
269
292
|
setParams(params: Record<string, string>): void;
|
|
270
293
|
getParams(): Record<string, string>;
|
|
271
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Record the committed history entry's location state from `entryState`:
|
|
297
|
+
* the state object the commit pushed, restored or merged into the entry,
|
|
298
|
+
* so the entry and its readers have one source. Notifies nobody:
|
|
299
|
+
* NavigationProvider takes the snapshot with the entry's payload
|
|
300
|
+
* (LocationStateContext). `treeless` marks a commit no payload follows (a
|
|
301
|
+
* shallow navigation, a commit that keeps every segment, an action's
|
|
302
|
+
* state): the provider takes it with this commit's state notification, in
|
|
303
|
+
* the batch of the location.
|
|
304
|
+
*/
|
|
305
|
+
commitLocationState(entryState: unknown, treeless?: boolean): void;
|
|
306
|
+
getLocationState(): LocationStateSnapshot;
|
|
307
|
+
/** True once after a `treeless` commit. */
|
|
308
|
+
takeTreelessLocationState(): boolean;
|
|
309
|
+
|
|
272
310
|
// Direct state access for advanced use
|
|
273
311
|
getCurrentNavigation(): NavigationEntry | null;
|
|
274
312
|
getInflightActions(): Map<string, ActionEntry>;
|
|
@@ -460,9 +498,14 @@ export function createEventController(
|
|
|
460
498
|
let handleData: HandleData = {};
|
|
461
499
|
let handleSegmentOrder: string[] = [];
|
|
462
500
|
let routeSegmentIds: string[] = [];
|
|
501
|
+
// freezeHydrationHandleState: what the document's HTML was rendered with.
|
|
502
|
+
let hydrationHandleState: HandleState | undefined;
|
|
463
503
|
|
|
464
504
|
let routeParams: Record<string, string> = {};
|
|
465
505
|
|
|
506
|
+
let locationState: LocationStateSnapshot;
|
|
507
|
+
let treelessLocationState = false;
|
|
508
|
+
|
|
466
509
|
const stateListeners = new Set<StateListener>();
|
|
467
510
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
468
511
|
const handleListeners = new Set<HandleListener>();
|
|
@@ -992,7 +1035,11 @@ export function createEventController(
|
|
|
992
1035
|
const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
|
|
993
1036
|
|
|
994
1037
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
995
|
-
// Partial update: merge new data with existing
|
|
1038
|
+
// Partial update: merge new data with existing, into a copy of the
|
|
1039
|
+
// containers. The object a reader was handed is never written to: the
|
|
1040
|
+
// hydration snapshot (freezeHydrationHandleState) and a history entry
|
|
1041
|
+
// restored from the store's cache can be that object.
|
|
1042
|
+
handleData = cloneHandleData(handleData);
|
|
996
1043
|
for (const handleName of Object.keys(data)) {
|
|
997
1044
|
if (!handleData[handleName]) {
|
|
998
1045
|
handleData[handleName] = {};
|
|
@@ -1094,6 +1141,20 @@ export function createEventController(
|
|
|
1094
1141
|
return routeParams;
|
|
1095
1142
|
}
|
|
1096
1143
|
|
|
1144
|
+
function commitLocationState(entryState: unknown, treeless?: boolean): void {
|
|
1145
|
+
locationState = locationStateSnapshot(entryState, locationState);
|
|
1146
|
+
if (treeless) {
|
|
1147
|
+
treelessLocationState = true;
|
|
1148
|
+
notify();
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function takeTreelessLocationState(): boolean {
|
|
1153
|
+
const taken = treelessLocationState;
|
|
1154
|
+
treelessLocationState = false;
|
|
1155
|
+
return taken;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1097
1158
|
// ========================================================================
|
|
1098
1159
|
// Return Controller
|
|
1099
1160
|
// ========================================================================
|
|
@@ -1116,12 +1177,24 @@ export function createEventController(
|
|
|
1116
1177
|
// Handles
|
|
1117
1178
|
setHandleData,
|
|
1118
1179
|
getHandleState,
|
|
1180
|
+
freezeHydrationHandleState: () => {
|
|
1181
|
+
hydrationHandleState = getHandleState();
|
|
1182
|
+
},
|
|
1183
|
+
getHydrationHandleState: () =>
|
|
1184
|
+
hydrationHandleState?.data === handleData
|
|
1185
|
+
? undefined
|
|
1186
|
+
: hydrationHandleState,
|
|
1119
1187
|
setRouteSegmentIds,
|
|
1120
1188
|
|
|
1121
1189
|
// Params
|
|
1122
1190
|
setParams,
|
|
1123
1191
|
getParams,
|
|
1124
1192
|
|
|
1193
|
+
// Location state
|
|
1194
|
+
commitLocationState,
|
|
1195
|
+
getLocationState: () => locationState,
|
|
1196
|
+
takeTreelessLocationState,
|
|
1197
|
+
|
|
1125
1198
|
// Subscriptions
|
|
1126
1199
|
subscribe,
|
|
1127
1200
|
subscribeToAction,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
addLocationState,
|
|
3
|
+
CLEAR_ON_RELOAD_KEY_SUFFIX,
|
|
2
4
|
isLocationStateDefinition,
|
|
3
5
|
isLocationStateEntry,
|
|
6
|
+
isLocationStateKey,
|
|
4
7
|
peekLocationStateKey,
|
|
5
8
|
replaceCurrentHistoryState,
|
|
6
9
|
resolveLocationStateEntries,
|
|
10
|
+
stampLocationState,
|
|
7
11
|
} from "./react/location-state-shared.js";
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -13,7 +17,7 @@ function isTypedLocationState(
|
|
|
13
17
|
state: unknown,
|
|
14
18
|
): state is Record<string, unknown> {
|
|
15
19
|
if (state === null || typeof state !== "object") return false;
|
|
16
|
-
return Object.keys(state).some(
|
|
20
|
+
return Object.keys(state).some(isLocationStateKey);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -100,16 +104,15 @@ export function buildHistoryState(
|
|
|
100
104
|
Object.assign(result, serverState);
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
if (hasLocationState(result)) stampLocationState(result);
|
|
108
|
+
|
|
103
109
|
return Object.keys(result).length > 0 ? result : null;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
/** Check if a history state object contains location state keys. */
|
|
107
113
|
export function hasLocationState(state: unknown): boolean {
|
|
108
114
|
if (!state || typeof state !== "object") return false;
|
|
109
|
-
return (
|
|
110
|
-
"state" in state ||
|
|
111
|
-
Object.keys(state).some((k) => k.startsWith("__rsc_ls_"))
|
|
112
|
-
);
|
|
115
|
+
return "state" in state || Object.keys(state).some(isLocationStateKey);
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/**
|
|
@@ -157,19 +160,38 @@ export function stripShellMissMarker(): void {
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* so
|
|
163
|
+
* Remove the `clearOnReload` slots of the entry a document load starts on:
|
|
164
|
+
* the server rendered this document without them. One pass before hydration
|
|
165
|
+
* (initBrowserApp; renderRoute's `hydrate` mode), so no reader needs to know
|
|
166
|
+
* the page began as a document load, and a slot nobody reads is removed too.
|
|
167
|
+
* No server-set state exists yet to lose: a document response carries none
|
|
168
|
+
* (rsc-rendering.ts attaches it to partial payloads only).
|
|
169
|
+
*
|
|
170
|
+
* A slot is recognised by its key alone (the `~r` suffix): at start-up the
|
|
171
|
+
* module that defines it may not be loaded.
|
|
172
|
+
*/
|
|
173
|
+
export function clearLocationStateOnDocumentLoad(): void {
|
|
174
|
+
const state: unknown = window.history.state;
|
|
175
|
+
if (state === null || typeof state !== "object") return;
|
|
176
|
+
let next: Record<string, unknown> | undefined;
|
|
177
|
+
for (const key in state) {
|
|
178
|
+
if (isLocationStateKey(key) && key.endsWith(CLEAR_ON_RELOAD_KEY_SUFFIX)) {
|
|
179
|
+
next ??= { ...state };
|
|
180
|
+
delete next[key];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (next) window.history.replaceState(next, "");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Merge server-set location state into the current history entry and return
|
|
188
|
+
* the entry state written. Readers see it when the caller commits it
|
|
189
|
+
* (EventController.commitLocationState).
|
|
163
190
|
*/
|
|
164
191
|
export function mergeLocationState(
|
|
165
192
|
locationState: Record<string, unknown>,
|
|
166
|
-
):
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
171
|
-
replaceCurrentHistoryState(merged);
|
|
172
|
-
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
173
|
-
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
174
|
-
}
|
|
193
|
+
): Record<string, unknown> {
|
|
194
|
+
const entryState = addLocationState(window.history.state, locationState);
|
|
195
|
+
replaceCurrentHistoryState(entryState);
|
|
196
|
+
return entryState;
|
|
175
197
|
}
|
|
@@ -14,11 +14,7 @@ import {
|
|
|
14
14
|
createNavigationTransaction,
|
|
15
15
|
resolveNavigationState,
|
|
16
16
|
} from "./navigation-transaction.js";
|
|
17
|
-
import {
|
|
18
|
-
buildHistoryState,
|
|
19
|
-
hasLocationState,
|
|
20
|
-
pushHistoryWithIdx,
|
|
21
|
-
} from "./history-state.js";
|
|
17
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
22
18
|
import {
|
|
23
19
|
handleNavigationStart,
|
|
24
20
|
handleNavigationEnd,
|
|
@@ -220,8 +216,6 @@ export function createNavigationBridge(
|
|
|
220
216
|
// Save current scroll position before changing URL
|
|
221
217
|
handleNavigationStart();
|
|
222
218
|
|
|
223
|
-
// Snapshot old state before pushState/replaceState overwrites it
|
|
224
|
-
const oldState = window.history.state;
|
|
225
219
|
const shallowRouteName = store.getHistoryEntryMemory()?.routeName;
|
|
226
220
|
|
|
227
221
|
// Update browser URL (carry intercept context into history state)
|
|
@@ -239,11 +233,8 @@ export function createNavigationBridge(
|
|
|
239
233
|
ensureHistoryKey();
|
|
240
234
|
// Same route, new entry (transition({ when }) back/forward source).
|
|
241
235
|
store.rememberDisplayedEntry(shallowRouteName);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (hasLocationState(oldState) || hasLocationState(historyState)) {
|
|
245
|
-
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
246
|
-
}
|
|
236
|
+
// Treeless: readers take the entry's state with setLocation below.
|
|
237
|
+
eventController.commitLocationState(historyState, true);
|
|
247
238
|
|
|
248
239
|
// Update store history key so future navigations reference the right cache
|
|
249
240
|
store.setHistoryKey(historyKey);
|
|
@@ -357,6 +348,8 @@ export function createNavigationBridge(
|
|
|
357
348
|
const clientUrlPresentation = beginClientUrlNavigation(
|
|
358
349
|
targetUrl,
|
|
359
350
|
tx.handle.signal,
|
|
351
|
+
// The entry the commit will push, without what the server adds.
|
|
352
|
+
() => buildHistoryState(resolvedState),
|
|
360
353
|
transitionOptOut
|
|
361
354
|
? () => true
|
|
362
355
|
: (destination) =>
|
|
@@ -378,7 +371,7 @@ export function createNavigationBridge(
|
|
|
378
371
|
url: targetUrl,
|
|
379
372
|
params: { ...params, ...destination.params },
|
|
380
373
|
routeName: destination.routeName,
|
|
381
|
-
state:
|
|
374
|
+
state: destination.state,
|
|
382
375
|
};
|
|
383
376
|
},
|
|
384
377
|
extra: destination.when,
|
|
@@ -578,9 +571,6 @@ export function createNavigationBridge(
|
|
|
578
571
|
historyKey,
|
|
579
572
|
);
|
|
580
573
|
|
|
581
|
-
// Update location in event controller
|
|
582
|
-
eventController.setLocation(new URL(url));
|
|
583
|
-
|
|
584
574
|
// If this is an intercept, restore the intercept context
|
|
585
575
|
if (isIntercept && interceptSourceUrl) {
|
|
586
576
|
store.setInterceptSourceUrl(interceptSourceUrl);
|
|
@@ -648,9 +638,16 @@ export function createNavigationBridge(
|
|
|
648
638
|
transitionGatedOff: gatedOff,
|
|
649
639
|
});
|
|
650
640
|
store.rememberDisplayedEntry();
|
|
651
|
-
//
|
|
652
|
-
//
|
|
641
|
+
// The restored entry's location and params, set before onUpdate so
|
|
642
|
+
// the hooks take both in the batch that renders its tree. Not at
|
|
643
|
+
// the popstate event: the page being left is on screen until here
|
|
644
|
+
// (#1031). A restore that has to fetch gets its location from
|
|
645
|
+
// tx.commit() instead (navigation-transaction.ts).
|
|
646
|
+
eventController.setLocation(new URL(url));
|
|
653
647
|
eventController.setParams(cachedParams);
|
|
648
|
+
// The entry this handler found at the event, which the cached tree
|
|
649
|
+
// was built for: history may have moved during the render above.
|
|
650
|
+
eventController.commitLocationState(historyState);
|
|
654
651
|
|
|
655
652
|
const popstateUpdate = {
|
|
656
653
|
root,
|
|
@@ -783,6 +780,32 @@ export function createNavigationBridge(
|
|
|
783
780
|
return;
|
|
784
781
|
}
|
|
785
782
|
|
|
783
|
+
// The browser is already on the entry that redirected. Replacing makes
|
|
784
|
+
// that entry the redirect target, so Back from the target reaches the
|
|
785
|
+
// entry before it; a push would leave the redirecting entry in history
|
|
786
|
+
// and Back would hit the redirect again. Before setLocation below, or
|
|
787
|
+
// the hooks would report the redirecting URL first.
|
|
788
|
+
if (error instanceof ServerRedirect) {
|
|
789
|
+
const redirectUrl = validateRedirectOrigin(
|
|
790
|
+
error.url,
|
|
791
|
+
window.location.origin,
|
|
792
|
+
);
|
|
793
|
+
if (!redirectUrl) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
return this.navigate(redirectUrl, {
|
|
797
|
+
state: error.state,
|
|
798
|
+
replace: true,
|
|
799
|
+
_skipCache: true,
|
|
800
|
+
} as NavigateOptionsInternal);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// tx.commit() never ran, so the location is still the page being
|
|
804
|
+
// left. The error boundary below replaces that page, and history is
|
|
805
|
+
// already on the entry that failed: the hooks report that entry, in
|
|
806
|
+
// the boundary's batch.
|
|
807
|
+
eventController.setLocation(new URL(url));
|
|
808
|
+
|
|
786
809
|
const networkError = toNetworkError(error, {
|
|
787
810
|
url,
|
|
788
811
|
operation: "navigation",
|
|
@@ -13,7 +13,6 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
14
|
import {
|
|
15
15
|
buildHistoryState,
|
|
16
|
-
hasLocationState,
|
|
17
16
|
mergeLocationState,
|
|
18
17
|
pushHistoryWithIdx,
|
|
19
18
|
} from "./history-state.js";
|
|
@@ -59,6 +58,11 @@ interface CommitOptions {
|
|
|
59
58
|
* partial updater reuses it instead of deciding again.
|
|
60
59
|
*/
|
|
61
60
|
transitionGatedOff?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* No payload follows this commit: every segment is kept and the tree on
|
|
63
|
+
* screen stays (EventController.commitLocationState).
|
|
64
|
+
*/
|
|
65
|
+
treeless?: boolean;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -81,6 +85,8 @@ interface BoundCommitOverrides {
|
|
|
81
85
|
serverState?: Record<string, unknown>;
|
|
82
86
|
/** The committed route's name (payload metadata), remembered per history entry. */
|
|
83
87
|
routeName?: string;
|
|
88
|
+
/** No payload follows this commit (CommitOptions.treeless). */
|
|
89
|
+
treeless?: boolean;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -187,10 +193,13 @@ export function createNavigationTransaction(
|
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
if (traversal) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
// The entry as history restored it, plus what the server adds.
|
|
197
|
+
const entryState: unknown =
|
|
198
|
+
serverState && Object.keys(serverState).length > 0
|
|
199
|
+
? mergeLocationState(serverState)
|
|
200
|
+
: window.history.state;
|
|
193
201
|
store.rememberDisplayedEntry(opts.routeName);
|
|
202
|
+
eventController.commitLocationState(entryState, opts.treeless);
|
|
194
203
|
handle.complete(parsedUrl);
|
|
195
204
|
debugLog("[Browser] Traversal committed, historyKey:", historyKey);
|
|
196
205
|
return { scroll };
|
|
@@ -202,15 +211,10 @@ export function createNavigationTransaction(
|
|
|
202
211
|
serverState,
|
|
203
212
|
);
|
|
204
213
|
|
|
205
|
-
const oldState = window.history.state;
|
|
206
|
-
|
|
207
214
|
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
208
215
|
ensureHistoryKey();
|
|
209
216
|
store.rememberDisplayedEntry(opts.routeName);
|
|
210
|
-
|
|
211
|
-
if (hasLocationState(oldState) || hasLocationState(historyState)) {
|
|
212
|
-
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
213
|
-
}
|
|
217
|
+
eventController.commitLocationState(historyState, opts.treeless);
|
|
214
218
|
|
|
215
219
|
handle.complete(parsedUrl);
|
|
216
220
|
|
|
@@ -269,6 +273,7 @@ export function createNavigationTransaction(
|
|
|
269
273
|
cacheOnly,
|
|
270
274
|
serverState,
|
|
271
275
|
routeName: overrides?.routeName ?? opts.routeName,
|
|
276
|
+
treeless: overrides?.treeless,
|
|
272
277
|
});
|
|
273
278
|
},
|
|
274
279
|
};
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type TransitionWhenActionInput,
|
|
43
43
|
} from "./transition-when.js";
|
|
44
44
|
import { buildHistoryState } from "./history-state.js";
|
|
45
|
+
import { addLocationState } from "./react/location-state-shared.js";
|
|
45
46
|
import type { TransitionWhenKind } from "../types/segments.js";
|
|
46
47
|
|
|
47
48
|
function toScrollPayload(
|
|
@@ -225,8 +226,14 @@ export function createPartialUpdater(
|
|
|
225
226
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
// The page the client is on, which the server diffs the target against.
|
|
230
|
+
// tx.currentUrl is window.location when the transaction was created: on
|
|
231
|
+
// a back/forward the popstate event had already moved it to the target,
|
|
232
|
+
// and a server told the target is also the source revalidates nothing on
|
|
233
|
+
// the same route, so the page being left stayed on screen (#1030). The
|
|
234
|
+
// store's URL is the committed entry's until tx.commit().
|
|
228
235
|
const previousUrl =
|
|
229
|
-
mode.type === "leave-intercept"
|
|
236
|
+
mode.type === "leave-intercept" || (tx.traversal && !interceptSourceUrl)
|
|
230
237
|
? segmentState.currentUrl || tx.currentUrl
|
|
231
238
|
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
232
239
|
|
|
@@ -279,7 +286,7 @@ export function createPartialUpdater(
|
|
|
279
286
|
params: metadata.params ?? mergeSegmentParams(segmentsToCommit),
|
|
280
287
|
routeName: metadata.routeName,
|
|
281
288
|
state: tx.traversal
|
|
282
|
-
?
|
|
289
|
+
? addLocationState(window.history.state, metadata.locationState)
|
|
283
290
|
: buildHistoryState(tx.state, undefined, metadata.locationState),
|
|
284
291
|
}),
|
|
285
292
|
action:
|
|
@@ -499,6 +506,7 @@ export function createPartialUpdater(
|
|
|
499
506
|
);
|
|
500
507
|
tx.commit(matchedIds, existingSegments, {
|
|
501
508
|
routeName: payload.metadata.routeName,
|
|
509
|
+
treeless: true,
|
|
502
510
|
});
|
|
503
511
|
debugLog("[Browser] Navigation complete (no re-render)");
|
|
504
512
|
return;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* cache keys responses by (URL, X-Rango-State value).
|
|
8
8
|
*
|
|
9
9
|
* Value format: `{buildVersion}:{invalidationTimestamp}`
|
|
10
|
-
* - Build version
|
|
10
|
+
* - Build version: the router's document version. It changes when a deploy
|
|
11
|
+
* changes the router's server code or the client assets, busting all cached
|
|
12
|
+
* prefetches at boot; a deploy that changes neither keeps them.
|
|
11
13
|
* - Timestamp rotates on invalidation (server action, invalidateClientCache).
|
|
12
14
|
*
|
|
13
15
|
* Storage is a session cookie named by the server-resolved name passed to
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
subscribeToPrefetchScopeChange,
|
|
21
21
|
} from "../link-interceptor.js";
|
|
22
22
|
import { subscribeToLocationChange } from "../event-controller.js";
|
|
23
|
+
import { subscribeToNothing } from "./subscribe-to-nothing.js";
|
|
23
24
|
import { isLocationStateDefinition } from "./location-state-shared.js";
|
|
24
25
|
|
|
25
26
|
export type StateOrGetter<T = unknown> = T | (() => T);
|
|
@@ -51,8 +52,6 @@ import type { PrefetchStrategy } from "../../router/prefetch-default.js";
|
|
|
51
52
|
export type { PrefetchStrategy } from "../../router/prefetch-default.js";
|
|
52
53
|
export { resolveAdaptiveStrategy } from "../prefetch/default-strategy.js";
|
|
53
54
|
|
|
54
|
-
const IGNORE_STRATEGY_CHANGES = (_listener: () => void) => () => {};
|
|
55
|
-
|
|
56
55
|
/**
|
|
57
56
|
* Link component props
|
|
58
57
|
*/
|
|
@@ -274,7 +273,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
274
273
|
const resolvedStrategy = useSyncExternalStore(
|
|
275
274
|
configuredStrategy === "adaptive"
|
|
276
275
|
? subscribeToAdaptiveStrategyChange
|
|
277
|
-
:
|
|
276
|
+
: subscribeToNothing,
|
|
278
277
|
() => resolveAdaptiveStrategy(configuredStrategy),
|
|
279
278
|
() => (configuredStrategy === "adaptive" ? "hover" : configuredStrategy),
|
|
280
279
|
);
|
|
@@ -11,6 +11,7 @@ import React, {
|
|
|
11
11
|
type ReactNode,
|
|
12
12
|
} from "react";
|
|
13
13
|
import {
|
|
14
|
+
LocationStateContext,
|
|
14
15
|
NavigationStoreContext,
|
|
15
16
|
type NavigationStoreContextValue,
|
|
16
17
|
} from "./context.js";
|
|
@@ -315,6 +316,11 @@ export function NavigationProvider({
|
|
|
315
316
|
}: NavigationProviderProps): ReactNode {
|
|
316
317
|
// Track current payload for rendering (this triggers re-renders)
|
|
317
318
|
const [payload, setPayload] = useState(initialPayload);
|
|
319
|
+
// The location state of the entry `payload` renders (LocationStateContext).
|
|
320
|
+
// Only ever set next to setPayload, or for a commit that has no payload.
|
|
321
|
+
const [locationState, setLocationState] = useState(
|
|
322
|
+
eventController.getLocationState,
|
|
323
|
+
);
|
|
318
324
|
|
|
319
325
|
/**
|
|
320
326
|
* Navigate to a URL (delegates to bridge)
|
|
@@ -373,6 +379,19 @@ export function NavigationProvider({
|
|
|
373
379
|
return startConnectionWarmup();
|
|
374
380
|
}, [warmupEnabled]);
|
|
375
381
|
|
|
382
|
+
// A commit no payload follows (a shallow navigation, a commit that keeps
|
|
383
|
+
// every segment, merged server-set state): the tree on screen is already
|
|
384
|
+
// the entry's, so its state is taken with the commit's notification.
|
|
385
|
+
useEffect(
|
|
386
|
+
() =>
|
|
387
|
+
eventController.subscribe(() => {
|
|
388
|
+
if (eventController.takeTreelessLocationState()) {
|
|
389
|
+
setLocationState(eventController.getLocationState());
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
392
|
+
[eventController],
|
|
393
|
+
);
|
|
394
|
+
|
|
376
395
|
// Cancel non-matching prefetches when navigation starts.
|
|
377
396
|
// Frees connections so the navigation fetch isn't competing with
|
|
378
397
|
// speculative prefetches. The prefetch matching the navigation target
|
|
@@ -421,6 +440,9 @@ export function NavigationProvider({
|
|
|
421
440
|
root: update.root,
|
|
422
441
|
metadata: update.metadata,
|
|
423
442
|
});
|
|
443
|
+
// The state of the entry this payload renders, as its commit recorded
|
|
444
|
+
// it: one update, so one lane, so one React commit with the tree.
|
|
445
|
+
setLocationState(eventController.getLocationState());
|
|
424
446
|
|
|
425
447
|
// Update route params. Only reset when the server actually sends a params
|
|
426
448
|
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
@@ -506,7 +528,9 @@ export function NavigationProvider({
|
|
|
506
528
|
|
|
507
529
|
return (
|
|
508
530
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
509
|
-
{
|
|
531
|
+
<LocationStateContext.Provider value={locationState}>
|
|
532
|
+
{content}
|
|
533
|
+
</LocationStateContext.Provider>
|
|
510
534
|
</NavigationStoreContext.Provider>
|
|
511
535
|
);
|
|
512
536
|
}
|
|
@@ -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
|
|
@@ -66,3 +67,15 @@ export interface NavigationStoreContextValue {
|
|
|
66
67
|
*/
|
|
67
68
|
export const NavigationStoreContext: Context<NavigationStoreContextValue | null> =
|
|
68
69
|
createContext<NavigationStoreContextValue | null>(null);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The location state of the history entry whose tree is on screen, for
|
|
73
|
+
* useLocationState. NavigationProvider holds it as React state next to the
|
|
74
|
+
* payload and sets both in one update, so a render sees an entry's state
|
|
75
|
+
* exactly when it sees that entry's tree: a transition React holds renders
|
|
76
|
+
* the destination's value, an urgent render of the content still on screen
|
|
77
|
+
* (a reader mounting there included) the value of the entry being left
|
|
78
|
+
* (#1029). Undefined on the server and outside a provider.
|
|
79
|
+
*/
|
|
80
|
+
export const LocationStateContext: Context<LocationStateSnapshot> =
|
|
81
|
+
createContext<LocationStateSnapshot>(undefined);
|