@rangojs/router 0.19.1 → 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/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- 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 +47 -6
- 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 +21 -2
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +24 -2
- 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 +1544 -420
- package/package.json +1 -2
- package/skills/bundle-analysis/SKILL.md +1 -1
- 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 +23 -3
- 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/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- 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 +48 -6
- 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 +40 -7
- package/src/vite/rango.ts +30 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +84 -14
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -217,7 +217,6 @@
|
|
|
217
217
|
"@playwright/test": "^1.49.1",
|
|
218
218
|
"@testing-library/react": ">=16",
|
|
219
219
|
"@vercel/functions": "^3.0.0",
|
|
220
|
-
"@vitejs/plugin-rsc": "^0.5.35",
|
|
221
220
|
"react": ">=19.2.8 <20",
|
|
222
221
|
"react-dom": ">=19.2.8 <20",
|
|
223
222
|
"vite": "^8.0.16",
|
|
@@ -15,7 +15,7 @@ Your app builds in three Vite environments — `client`, `ssr`, and `rsc` — an
|
|
|
15
15
|
1. **Server code leaking into the client.** A file that imports `node:fs`, calls a database, or contains action logic ends up in the client bundle because a client component pulled it in transitively. Symptom: your client bundle is much larger than expected, sometimes with imports that fail at runtime.
|
|
16
16
|
2. **Both dev and prod React in the SSR/RSC bundle.** When `process.env.NODE_ENV` isn't folded at build time, React's CJS files ship both `.development.js` _and_ `.production.js` variants — doubling React's footprint. The Cloudflare Vite plugin and rango's `vercel` preset fold NODE_ENV automatically; on the default `node` preset, `vite build` does it for the client but not always for SSR/RSC, so add the `define` from Step 2.
|
|
17
17
|
3. **An oversized routes-manifest in your RSC worker.** The `virtual:rsc-router/routes-manifest/<routerId>` chunk holds your route trie and precomputed entries — large only in proportion to your route count. If it's surprisingly big, you may have unintentionally generated routes (e.g., parametrized fixtures) that bloated the trie.
|
|
18
|
-
4. **Poor client-chunk grouping.** `rango({ clientChunks })` decides how `"use client"` modules are grouped into browser chunks. The default groups them by route directory (`routes/<id>/**` → `app-<id>`), with flat folders like `src/components/` in one shared chunk; `clientChunks: false` ships one client chunk per router. A route that pulls in unrelated routes' client code, or one oversized shared chunk, points at the grouping.
|
|
18
|
+
4. **Poor client-chunk grouping.** `rango({ clientChunks })` decides how `"use client"` modules are grouped into browser chunks. The default groups them by route directory (`routes/<id>/**` → `app-<id>`), with flat folders like `src/components/` in one shared chunk; `clientChunks: false` ships one client chunk per router. A group is the loading unit: rendering any one member downloads the whole group's chunk. A route that pulls in unrelated routes' client code, or one oversized shared chunk, points at the grouping; `DEBUG=rango:chunks vite build` logs each group's size at build end.
|
|
19
19
|
|
|
20
20
|
Tree-shaking does _not_ catch generated data inlined as string literals, or data-dependent conditionals like React's `NODE_ENV` branches. You need a visualizer.
|
|
21
21
|
|
|
@@ -166,24 +166,87 @@ exemption there). The route then serves uncached; see `/ppr`.
|
|
|
166
166
|
|
|
167
167
|
### Cross-deploy safety: version-segmented store keys
|
|
168
168
|
|
|
169
|
-
`CFCacheStore`
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
The
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
the
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
169
|
+
`CFCacheStore` and `VercelCacheStore` prefix every **physical** store key with a
|
|
170
|
+
version, so a build only reads entries that code like its own wrote. The version
|
|
171
|
+
is a hash of the code, computed per `createRouter()` by `vite build`, not the
|
|
172
|
+
build time. Each router has two:
|
|
173
|
+
|
|
174
|
+
- The **data version** (a hash of the router's server code) keys cached RSC
|
|
175
|
+
data: segment entries from `cache()`, `"use cache"` values, loader data.
|
|
176
|
+
- The **document version** (the data version plus the SSR output, the client
|
|
177
|
+
asset file names, `base`, and the router's `Prerender` payloads) keys stored
|
|
178
|
+
HTML (PPR shells, document-cache responses). It is also sent to the browser,
|
|
179
|
+
and a mismatch with the tab's `_rsc_v` reloads the tab.
|
|
180
|
+
|
|
181
|
+
Same code builds to the same versions, so a rebuild or redeploy of unchanged
|
|
182
|
+
code keeps the cache, and a deploy that changes app A keeps app B's cache.
|
|
183
|
+
|
|
184
|
+
| Deploy | Cached data | Stored HTML | Open tabs |
|
|
185
|
+
| ------------------------------------- | ------------------- | --------------------- | ----------------------- |
|
|
186
|
+
| Rebuild, no code change | kept | kept | untouched |
|
|
187
|
+
| Server code of app A changes | cleared for A only | cleared for A only | A's tabs reload |
|
|
188
|
+
| Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
|
|
189
|
+
| Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
|
|
190
|
+
|
|
191
|
+
"Server code" is everything the app's server can run: its own modules, the
|
|
192
|
+
dependencies it imports (bundled, or on the node preset left external and
|
|
193
|
+
resolved from `node_modules`), and a stylesheet its server code links
|
|
194
|
+
(`import "./x.css"` in a server component, or `import href from "./x.css?url"`
|
|
195
|
+
for a document `<link>`): the stylesheet's hashed URL is in what the server
|
|
196
|
+
renders, so a change to the compiled CSS clears that app's cached data. A
|
|
197
|
+
`clientUrls()` module is client code that also tells the server what to run:
|
|
198
|
+
changing which loaders a route declares, its `loading` or its transition
|
|
199
|
+
counts as a server change; changing the components it renders does not. Under
|
|
200
|
+
a host router, apps are independent when the host mounts them lazily
|
|
201
|
+
(`.lazy(() => import(...))`); routers one module imports statically share
|
|
202
|
+
that module's code.
|
|
203
|
+
|
|
204
|
+
The key families: `CFCacheStore` keys segment entries and `fn:` items with the
|
|
205
|
+
data version and `doc:` responses and `shell2:` shells with the document
|
|
206
|
+
version. `VercelCacheStore` does the same by default (families `s`, `i` data;
|
|
207
|
+
`r`, `h` document). `MemorySegmentCacheStore` has no version; the process is its
|
|
208
|
+
scope.
|
|
209
|
+
|
|
210
|
+
Tag invalidation markers are stored without a version, so `updateTag()` and
|
|
211
|
+
`revalidateTag()` apply to entries of every version, including a version that
|
|
212
|
+
comes back in a rollback.
|
|
213
|
+
|
|
214
|
+
- **Force a clear:** change `version` (`createRouter({ version })`,
|
|
215
|
+
`createRSCHandler({ version })`, `new CFCacheStore({ version })` or
|
|
216
|
+
`new VercelCacheStore({ version })` all use that exact value for both
|
|
217
|
+
versions), or invalidate tags.
|
|
218
|
+
- **Keep the old "every deploy clears the cache" behavior:** set `version` to a
|
|
219
|
+
per-deploy value (a build id), or on Vercel keep a deployment-scoped
|
|
220
|
+
`getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
|
|
221
|
+
- **Separate environments:** two deployments built from the same code have the
|
|
222
|
+
same versions. If they share a store (the same KV namespace), they share
|
|
223
|
+
entries. Give each environment its own KV namespace or cache namespace, or its
|
|
224
|
+
own `version`.
|
|
225
|
+
|
|
226
|
+
Two things give a router a new version on every build, and with it a cold
|
|
227
|
+
cache on every deploy:
|
|
228
|
+
|
|
229
|
+
- **No stable encryption key.** The key that encrypts inline server-action
|
|
230
|
+
bound arguments is part of the version of every router whose code encrypts
|
|
231
|
+
with it. Pass `rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })`
|
|
232
|
+
(see `/use-cache`); the build prints a note when a router needs it.
|
|
233
|
+
- **A `Prerender` or `Static()` handler whose output differs per build**: a
|
|
234
|
+
timestamp, a random id, or an inline action with bound arguments rendered at
|
|
235
|
+
build time (its arguments are encrypted with a random IV, stable key or not).
|
|
236
|
+
- **An import the server build leaves external and cannot read**: a package
|
|
237
|
+
that is not in `node_modules` at build time, a path that does not resolve, a
|
|
238
|
+
URL. The build names it.
|
|
239
|
+
|
|
240
|
+
Every build prints each router's versions and writes what they were computed
|
|
241
|
+
from to `node_modules/.rangojs-router-build/cache-versions.json`. Diff that
|
|
242
|
+
file between two builds to see which chunk or payload moved a version; its
|
|
243
|
+
`unownedFiles` lists the server files in no router's version (the host entry
|
|
244
|
+
of lazily mounted apps), where a change keeps every app's cache.
|
|
245
|
+
|
|
246
|
+
A custom persistent store keys the same way with `getCacheVersions()` from
|
|
247
|
+
`@rangojs/router/cache`, called per operation inside the request: `data` for
|
|
248
|
+
segment entries and items, `document` for responses and shells, no version on
|
|
249
|
+
its tag-invalidation records. See `/caching` for store setup.
|
|
187
250
|
|
|
188
251
|
### Client cache: forward/back is mutation-aware
|
|
189
252
|
|
|
@@ -226,8 +289,9 @@ recompute for a merely-aging entry.
|
|
|
226
289
|
`/document-cache`. A platform CDN may independently consume the same header
|
|
227
290
|
and bypass the function on hits; see `/deployment-caching`.
|
|
228
291
|
|
|
229
|
-
SWR softens normal TTL expiry, **not** a
|
|
230
|
-
has no stale entry to serve (see
|
|
292
|
+
SWR softens normal TTL expiry, **not** a version change — a deploy that changes
|
|
293
|
+
a router's code reads under a new version and has no stale entry to serve (see
|
|
294
|
+
version-segmented store keys above).
|
|
231
295
|
|
|
232
296
|
Store support is layer-specific. `CFCacheStore` and `VercelCacheStore` support
|
|
233
297
|
SWR for segment, document/response, item (`"use cache"` and cached loaders), and
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -564,9 +564,13 @@ const router = createRouter<AppBindings>({
|
|
|
564
564
|
});
|
|
565
565
|
```
|
|
566
566
|
|
|
567
|
-
`CFCacheStore` prefixes every key with
|
|
568
|
-
|
|
569
|
-
|
|
567
|
+
`CFCacheStore` prefixes every key with a version computed per `createRouter()`
|
|
568
|
+
from that router's built code: segment entries and `fn:` items with the data
|
|
569
|
+
version, `doc:` responses and `shell2:` shells with the document version. A
|
|
570
|
+
deploy that changes a router's code reads under a new version; a deploy that does
|
|
571
|
+
not keeps the entries. Tag invalidation markers carry no version, so `updateTag()`
|
|
572
|
+
reaches entries of every version. `version` replaces both versions with one
|
|
573
|
+
value. See `/cache-guide` → "Cross-deploy safety" and `/cloudflare` for bindings.
|
|
570
574
|
|
|
571
575
|
### With KV L2 Persistence
|
|
572
576
|
|
|
@@ -755,10 +759,11 @@ the store adds SWR, tag invalidation, and the family split on top of it.
|
|
|
755
759
|
import { getCache, waitUntil } from "@vercel/functions";
|
|
756
760
|
import { VercelCacheStore } from "@rangojs/router/cache";
|
|
757
761
|
|
|
758
|
-
//
|
|
762
|
+
// No deployment id in the namespace: the store versions its keys itself, so a
|
|
763
|
+
// deploy that does not change a router keeps that router's entries.
|
|
759
764
|
// One handle per process: the store keeps its PPR shell and tag-marker
|
|
760
765
|
// memos per handle.
|
|
761
|
-
const runtimeCache = getCache(
|
|
766
|
+
const runtimeCache = getCache();
|
|
762
767
|
|
|
763
768
|
const router = createRouter({
|
|
764
769
|
document: Document,
|
|
@@ -794,6 +799,12 @@ beyond the per-item cap are dropped with a warning. The official client swallows
|
|
|
794
799
|
`expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
|
|
795
800
|
`/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
|
|
796
801
|
|
|
802
|
+
`VercelCacheStore` versions its keys by default: families `s` and `i` take the
|
|
803
|
+
router's data version, `r` and `h` its document version, and tag markers
|
|
804
|
+
(`rg:tm:{tag}`) take none. `version` replaces both. To clear the cache on every
|
|
805
|
+
deploy, set `version` to a per-deploy value or keep a deployment-scoped
|
|
806
|
+
`getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
|
|
807
|
+
|
|
797
808
|
## Cache purity & tainted objects
|
|
798
809
|
|
|
799
810
|
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
@@ -376,6 +376,22 @@ a hard load of the target URL renders the full route.
|
|
|
376
376
|
throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
|
|
377
377
|
(route-independent lane; `revalidate()` deliberately not consulted).
|
|
378
378
|
Pinned in the router repo's client-urls hook-probe e2e.
|
|
379
|
+
- **Location state follows the same split as the route hooks.** Inside the
|
|
380
|
+
optimistic branch (the destination and the group's client layouts)
|
|
381
|
+
`useLocationState` reads the state the navigation carries (`<Link state>`,
|
|
382
|
+
`router.push(url, { state })`) from the first optimistic render, before the
|
|
383
|
+
server answers: a product page can show the name its link carried while
|
|
384
|
+
its loader streams. Chrome outside the group, and content a navigation
|
|
385
|
+
keeps on screen (a same-route navigation, a destination that suspends with
|
|
386
|
+
no boundary), keep the committed entry's state until the canonical commit.
|
|
387
|
+
State a redirect carries arrives with that commit (an action's
|
|
388
|
+
`setLocationState` without a redirect is applied when its response
|
|
389
|
+
arrives), and a navigation that is superseded or cancelled leaves none
|
|
390
|
+
behind. A `flash` slot read in the branch is cleared from history once the
|
|
391
|
+
navigation commits, not before. In the branch the value is the object the
|
|
392
|
+
navigation passed; from the commit on it is the entry's copy (a structured
|
|
393
|
+
clone), so an object-valued slot is a new, equal object at that point.
|
|
394
|
+
Do not key an effect on its identity across the two.
|
|
379
395
|
- **Editing the module in dev:** projections refresh via HMR discovery; if a
|
|
380
396
|
route-shape edit ever serves stale routes, restart dev — and if you are
|
|
381
397
|
developing the router itself, rebuild the router dist before `pnpm dev`
|
|
@@ -48,7 +48,8 @@ export default defineConfig({
|
|
|
48
48
|
Worker runs the RSC entry and loads SSR inside the same isolate.
|
|
49
49
|
|
|
50
50
|
`buildEnv: "auto"` gives `Prerender` and `Static` handlers a real `ctx.env` at
|
|
51
|
-
build time through Wrangler's `getPlatformProxy()` (local bindings)
|
|
51
|
+
build time through Wrangler's `getPlatformProxy()` (local bindings), reading
|
|
52
|
+
the wrangler config from the Vite root whatever the working directory. Omit it
|
|
52
53
|
(default `false`) when build-time rendering must not touch bindings; `ctx.env`
|
|
53
54
|
then throws during build rendering. See `/prerender`.
|
|
54
55
|
|
|
@@ -57,7 +57,7 @@ and multi-region composition without replacing its routing or data model.
|
|
|
57
57
|
| Middleware | Global + segment-scoped subtree | Single root Proxy (matcher filters paths); no subtree scope | Request/server-function middleware | [Hono middleware + handler interceptors](https://waku.gg/#interceptors) |
|
|
58
58
|
| Observability | Built-in CF + Vercel OTel phase spans, `Server-Timing`, perf waterfall | Built-in OTel spans; no equivalent router-phase waterfall | Client/data devtools | No equivalent built-in phase model |
|
|
59
59
|
| Deploy targets | Node, Cloudflare, Vercel — all with presets | Node/Vercel first; other platforms through adapters | Broad Vite/Rsbuild runtime support | Node plus adapters |
|
|
60
|
-
| Deployment skew | Automatic
|
|
60
|
+
| Deployment skew | Automatic version handshake for navigation, prefetch, and actions; safe reload on mismatch | `deploymentId` mismatch reload; Vercel can pin clients to an immutable deploy | Application/platform policy | Build-id mismatch reload (v1.0-beta) |
|
|
61
61
|
| Multi-tenant | `createHostRouter()` built-in | Roll your own | Roll your own | Roll your own |
|
|
62
62
|
| Testing primitives | Ships `@rangojs/router/testing` (handlers/loaders/mw/Flight/e2e) | No comparable handler/Flight primitives | Documented patterns; no shipped utils | No comparable handler/Flight primitives |
|
|
63
63
|
| Client runtime | ~50 KB Rango + ~115 KB React/RSC, per-route chunks | Configuration-dependent | Configuration-dependent | Deliberately light |
|
|
@@ -336,8 +336,9 @@ first-class primitive.
|
|
|
336
336
|
|
|
337
337
|
### Deployment skew is detected before stale code executes
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
`vite build` computes a document version per `createRouter()` from that router's
|
|
340
|
+
built code (the server code, the SSR output, the client asset file names, `base`,
|
|
341
|
+
and its Prerender payloads) and ships it in the initial payload metadata. The browser returns that version on every partial navigation,
|
|
341
342
|
prefetch, and Server Action. If an old tab reaches a newer server, request
|
|
342
343
|
classification detects the mismatch before resolving the route or executing the
|
|
343
344
|
action and responds with `X-RSC-Reload`. The browser then performs a clean document
|
|
@@ -348,13 +349,16 @@ falling through to a misleading 404.
|
|
|
348
349
|
The protection covers fresh, completed-prefetch, and in-flight-prefetch responses.
|
|
349
350
|
For action requests, the reload returns to the same-origin referrer rather than the
|
|
350
351
|
internal action URL. The cache side follows the same correctness rule:
|
|
351
|
-
`CFCacheStore` automatically
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
`CFCacheStore` and `VercelCacheStore` automatically version their physical keys
|
|
353
|
+
(Cache API and KV keys on Cloudflare): cached RSC data (segment entries, `"use
|
|
354
|
+
cache"` values, loader data) by a data version, a hash of the router's server
|
|
355
|
+
code, and stored HTML (PPR shells, document-cache responses) by the document
|
|
356
|
+
version. A build whose router code changed cannot replay Flight containing an old
|
|
357
|
+
component shape or dead client-chunk reference, and a rebuild of unchanged code
|
|
358
|
+
keeps its cache. Tag invalidation markers carry no version, so `updateTag()`
|
|
359
|
+
reaches entries of every version. The `version` option replaces both versions.
|
|
360
|
+
The browser's Rango state also includes the document version, rotating HTTP and
|
|
361
|
+
in-memory prefetch cache identity on boot.
|
|
358
362
|
|
|
359
363
|
Next.js deserves explicit credit here: its
|
|
360
364
|
[`deploymentId`](https://nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId)
|
|
@@ -587,10 +591,10 @@ dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owni
|
|
|
587
591
|
link.
|
|
588
592
|
|
|
589
593
|
**Rango State ties prefetch and client-cache invalidation together.** It is a
|
|
590
|
-
session-cookie value shaped as `{
|
|
594
|
+
session-cookie value shaped as `{documentVersion}:{invalidationTimestamp}`. Navigation
|
|
591
595
|
and prefetch requests send it as `X-Rango-State`, and RSC responses vary on that
|
|
592
596
|
header, so the browser HTTP cache and Rango's decoded prefetch map share one cache
|
|
593
|
-
identity. A deployment changes the version; a mutation rotates the timestamp. Old
|
|
597
|
+
identity. A deployment that changes the router's code or client assets changes the version; a mutation rotates the timestamp. Old
|
|
594
598
|
responses become unreachable under the retired identity instead of requiring every
|
|
595
599
|
cache layer to delete the same entries successfully.
|
|
596
600
|
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -68,6 +68,9 @@ the one for your case.
|
|
|
68
68
|
- [`./state.md`](./state.md) — `useLocationState` (persistent + flash state,
|
|
69
69
|
`.read()`/`.read(location)`/`.write()`/`.delete()`, typed `state` on `router.push()` /
|
|
70
70
|
`router.replace()`, serializability errors that name the failing field),
|
|
71
|
+
state from another deploy (dropped by the router, no version to manage), the
|
|
72
|
+
`createLocationState` options (`flash`; `clearOnReload` for state that must
|
|
73
|
+
not come back after a refresh, such as a "load more" list),
|
|
71
74
|
`invalidateClientCache()`.
|
|
72
75
|
- [`./outlets.md`](./outlets.md) — `Outlet`, `ParallelOutlet`, `useOutlet`.
|
|
73
76
|
- [`./urls.md`](./urls.md) — `useParams`, `usePathname`, `useSearchParams`,
|
|
@@ -39,6 +39,12 @@ the capture, so its pushes always apply client-side; with `ssr: false` they
|
|
|
39
39
|
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
40
40
|
re-renders on every handle update, late loader pushes included.
|
|
41
41
|
|
|
42
|
+
On a document load `useHandle` hydrates with the handle data its HTML was
|
|
43
|
+
rendered from, then takes whatever arrived since. That holds wherever the
|
|
44
|
+
reader sits: a reader inside a `<Suspense>` or `loading()` boundary, which
|
|
45
|
+
hydrates after the rest of the page, does not see a late loader push during
|
|
46
|
+
its hydration render, so it cannot mismatch its server HTML.
|
|
47
|
+
|
|
42
48
|
Handles can be passed as props from server to client components:
|
|
43
49
|
|
|
44
50
|
```tsx
|
|
@@ -27,6 +27,34 @@ function IsLoading() {
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
#### Pending UI, Back/Forward included
|
|
31
|
+
|
|
32
|
+
A link click, `router.push()` and a Back/Forward that has to fetch its entry
|
|
33
|
+
report a pending navigation the same way. While the page being left is still
|
|
34
|
+
on screen:
|
|
35
|
+
|
|
36
|
+
| What you want to show | Read |
|
|
37
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
38
|
+
| a request is out, nothing has arrived | `state === "loading"`; `pendingUrl` is the URL being fetched |
|
|
39
|
+
| the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
|
|
40
|
+
| which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
|
|
41
|
+
|
|
42
|
+
`state` and `pendingUrl` cover the network wait. Once the response has
|
|
43
|
+
arrived a loader may still be streaming, and React keeps the page being left
|
|
44
|
+
on screen where a navigation holds its content (a same-route change, a
|
|
45
|
+
`transition()` route); `isStreaming` covers that part too, so use it for a
|
|
46
|
+
progress indicator that should last until the new page is complete.
|
|
47
|
+
|
|
48
|
+
On Back/Forward the address bar changes at once, before any of this: during
|
|
49
|
+
the wait `window.location` is the entry being fetched, the same URL as
|
|
50
|
+
`pendingUrl`, and `location` is the page on screen. A Back/Forward served
|
|
51
|
+
from the client's history cache restores its page in one commit and reports
|
|
52
|
+
no pending state.
|
|
53
|
+
|
|
54
|
+
`location` moves when the response arrives. Where React then holds the page
|
|
55
|
+
being left, it is ahead of the screen for that last part of the wait, and so
|
|
56
|
+
is a URL hook in a component that first mounts then (issue #1046).
|
|
57
|
+
|
|
30
58
|
### useRouter()
|
|
31
59
|
|
|
32
60
|
Access stable router actions. The returned object never changes identity, so
|
package/skills/hooks/state.md
CHANGED
|
@@ -84,6 +84,59 @@ definition:
|
|
|
84
84
|
const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### When a reader sees an entry's state
|
|
88
|
+
|
|
89
|
+
A reader sees a history entry's location state together with that entry's
|
|
90
|
+
page, never before it. The router keeps the state of the entry on screen next
|
|
91
|
+
to the page it renders and changes both in one React commit. While a
|
|
92
|
+
navigation is pending (its loader still streaming, a `transition()` holding
|
|
93
|
+
the page) what is on screen keeps the state of the entry being left. That
|
|
94
|
+
holds for every reader in it, including one that mounts during the wait: it
|
|
95
|
+
reads the entry on screen, not the one `history.state` already holds.
|
|
96
|
+
Back/forward and state the server sets on a navigation
|
|
97
|
+
(`ctx.setLocationState()`, `redirect(url, { state })`) follow the same rule.
|
|
98
|
+
|
|
99
|
+
| What changes the state | A reader gets it |
|
|
100
|
+
| ------------------------------------------------------------ | ---------------------------------------------------------------- |
|
|
101
|
+
| `<Link state>`, `router.push()` / `router.replace()` | in the commit that shows the destination |
|
|
102
|
+
| back/forward | in the commit that restores the entry (cached or refetched) |
|
|
103
|
+
| the server, on a navigation (`setLocationState`, a redirect) | in the commit that shows the destination |
|
|
104
|
+
| a navigation the server answers with nothing to re-render | with the new location: the page on screen is the entry's |
|
|
105
|
+
| `router.push(url, { revalidate: false })` (no server fetch) | with the new location: the page does not change |
|
|
106
|
+
| a server action's `setLocationState` (no navigation) | when the action's response arrives, added to what readers show |
|
|
107
|
+
| `Def.write()` / `Def.delete()` | when the entry is next restored: back/forward to it, or a reload |
|
|
108
|
+
|
|
109
|
+
Three consequences:
|
|
110
|
+
|
|
111
|
+
- A value keeps its identity until the entry's state changes it. A navigation
|
|
112
|
+
that carries no state re-renders no reader. One that does re-renders each
|
|
113
|
+
reader once, in the commit that already renders the page; a reader of a
|
|
114
|
+
slot the navigation carried forward unchanged (the value it got from the
|
|
115
|
+
hook, passed back) or of a slot a server action did not touch gets the same
|
|
116
|
+
object again, so its effects and memoized children do not run. Any other
|
|
117
|
+
object is a new value, equal content included: a link clicked twice
|
|
118
|
+
announces its state twice. A value is always a copy of what was passed.
|
|
119
|
+
- `Def.write()` and `Def.delete()` change the history entry and nothing a
|
|
120
|
+
reader shows, whether it is mounted or mounts later. `Def.read()` returns
|
|
121
|
+
the written value at once.
|
|
122
|
+
- Outside the router's provider (a second React root, a component rendered
|
|
123
|
+
bare in a test) a reader has no entry and returns `undefined`. Test with
|
|
124
|
+
`renderRoute` (see `/testing`).
|
|
125
|
+
|
|
126
|
+
A reader that mounts with the destination reads the destination's state on
|
|
127
|
+
its first render. Inside an optimistically rendered `clientUrls()` destination
|
|
128
|
+
that is the state the navigation carries, before the server has answered (see
|
|
129
|
+
`/client-urls`, "Pitfalls").
|
|
130
|
+
|
|
131
|
+
The URL hooks follow the same rule for a component that is mounted across the
|
|
132
|
+
navigation: on back/forward too, `usePathname()` and `useSearchParams()`
|
|
133
|
+
change with the page, not at the `popstate` event (see
|
|
134
|
+
[`./urls.md`](./urls.md), "When the URL hooks change"). One difference
|
|
135
|
+
remains (issue #1046): a component that first mounts in the page being left,
|
|
136
|
+
after the response arrived and while React still holds the destination, reads
|
|
137
|
+
its own entry's location state from `useLocationState` and the destination's
|
|
138
|
+
URL and params from `usePathname()`, `useSearchParams()` and `useParams()`.
|
|
139
|
+
|
|
87
140
|
### State on router.push() / router.replace()
|
|
88
141
|
|
|
89
142
|
The same `state` option exists on `router.push()` / `router.replace()` (see
|
|
@@ -201,9 +254,11 @@ Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
|
201
254
|
hook reads it. `useLocationState` reads the value during render (on the
|
|
202
255
|
hydration render it returns `undefined` and reads in a post-mount effect, so
|
|
203
256
|
SSR and hydration agree), then clears it from `history.state` via
|
|
204
|
-
`replaceState` in a `useEffect`.
|
|
205
|
-
definition
|
|
206
|
-
|
|
257
|
+
`replaceState` in a `useEffect`. Every component reading the same flash
|
|
258
|
+
definition sees the value, whenever it mounts, until the next navigation or
|
|
259
|
+
back/forward commits its entry's state: an entry that carries no flash shows
|
|
260
|
+
none. A server action's state does not end it. Pressing back/forward will not
|
|
261
|
+
re-show the flash since it was cleared.
|
|
207
262
|
|
|
208
263
|
Set flash state from the server via `redirect()` with state (from a handler,
|
|
209
264
|
middleware, or server action). Import the definition from the shared module so
|
|
@@ -221,7 +276,9 @@ import { FlashMessage } from "../location-states";
|
|
|
221
276
|
};
|
|
222
277
|
```
|
|
223
278
|
|
|
224
|
-
Or via `ctx.setLocationState()`
|
|
279
|
+
Or via `ctx.setLocationState()` from a handler or middleware. It is delivered
|
|
280
|
+
with a client navigation's or an action's response; a document response
|
|
281
|
+
carries no location state (there is no history entry to write it to yet):
|
|
225
282
|
|
|
226
283
|
```tsx
|
|
227
284
|
(ctx) => {
|
|
@@ -230,6 +287,148 @@ Or via `ctx.setLocationState()` on any response (handlers and middleware):
|
|
|
230
287
|
};
|
|
231
288
|
```
|
|
232
289
|
|
|
290
|
+
### State from another deploy
|
|
291
|
+
|
|
292
|
+
History entries survive reloads and back/forward, so a tab left open across a
|
|
293
|
+
release still holds the state the older build wrote. The router drops it for
|
|
294
|
+
you: every entry records the app version its location state was written under,
|
|
295
|
+
and a read under any other version returns `undefined`, for typed slots and
|
|
296
|
+
for plain state alike. You do not manage a version and there is none to read;
|
|
297
|
+
the app version is the one the router already uses to reload a client that is
|
|
298
|
+
behind the server. It is computed from the app's built code (its server code
|
|
299
|
+
and the client assets), so it changes with a build that changes the app and
|
|
300
|
+
stays the same across a rebuild or redeploy of unchanged code.
|
|
301
|
+
|
|
302
|
+
| An entry's location state, read by | Result |
|
|
303
|
+
| ---------------------------------------------------------------- | ----------- |
|
|
304
|
+
| the version that wrote it: navigation, back/forward, refresh | the value |
|
|
305
|
+
| the same code rebuilt or redeployed | the value |
|
|
306
|
+
| a changed build, after a refresh or a restored tab | `undefined` |
|
|
307
|
+
| a changed build, on back/forward to an entry the older one wrote | `undefined` |
|
|
308
|
+
| any build, when the entry predates this behavior | `undefined` |
|
|
309
|
+
|
|
310
|
+
So location state is for the current session of the current version of the
|
|
311
|
+
app: a filter carried on a link, a flash message, a scroll target. State that
|
|
312
|
+
has to outlive a deploy that changes the app belongs in the URL, a cookie or
|
|
313
|
+
storage. In development a
|
|
314
|
+
server-module edit (HMR) changes the version too; the running page keeps its
|
|
315
|
+
state, and the next document load drops it.
|
|
316
|
+
|
|
317
|
+
### createLocationState options
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
createLocationState<TState>({
|
|
321
|
+
flash?: boolean, // read once, cleared after paint (above)
|
|
322
|
+
clearOnReload?: boolean, // drop the state on a document load (below)
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The value is always stored as-is. `clearOnReload` changes the slot's KEY in
|
|
327
|
+
`history.state` instead, by a suffix on the key the Vite plugin injects
|
|
328
|
+
(`<key>` below, `__rsc_ls_<file>#<ExportName>`):
|
|
329
|
+
|
|
330
|
+
| Options | Key in `history.state` | Reads `undefined` when |
|
|
331
|
+
| --------------- | ---------------------- | ----------------------------------------------- |
|
|
332
|
+
| none, `flash` | `<key>` | the slot is empty (`flash`: once read) |
|
|
333
|
+
| `clearOnReload` | `<key>~r` | the entry's document was loaded since the write |
|
|
334
|
+
|
|
335
|
+
`Def.__rsc_ls_key` is that key, suffix included. `~` cannot appear in an export
|
|
336
|
+
name, so no other definition can end up with a suffixed key.
|
|
337
|
+
|
|
338
|
+
`flash` with `clearOnReload` is rejected: `createLocationState` throws in
|
|
339
|
+
development.
|
|
340
|
+
|
|
341
|
+
### clearOnReload: state the server did not render with
|
|
342
|
+
|
|
343
|
+
Location state lives in the browser, so the server renders a document without
|
|
344
|
+
it. `useLocationState` therefore hydrates as `undefined` and applies the stored
|
|
345
|
+
value right after. For most state that is invisible. For state that decides
|
|
346
|
+
how much content is on the page, it is a layout shift on every refresh.
|
|
347
|
+
|
|
348
|
+
The case this option exists for is a "load more" list. `?page=6` loads that
|
|
349
|
+
page's 50 products through a loader, and the 250 already on screen ride along
|
|
350
|
+
as location state on the link, so the next page shows them at once and
|
|
351
|
+
streams the new ones:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// location-states.ts
|
|
355
|
+
import { createLocationState } from "@rangojs/router";
|
|
356
|
+
|
|
357
|
+
export const CarriedProducts = createLocationState<Product[]>({
|
|
358
|
+
clearOnReload: true,
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
"use client";
|
|
364
|
+
import { Link, useLoader, useLocationState } from "@rangojs/router/client";
|
|
365
|
+
import { ProductsLoader } from "./loaders"; // the page named by ?page
|
|
366
|
+
import { CarriedProducts } from "./location-states";
|
|
367
|
+
|
|
368
|
+
export function ProductList() {
|
|
369
|
+
const { data } = useLoader(ProductsLoader);
|
|
370
|
+
const carried = useLocationState(CarriedProducts) ?? [];
|
|
371
|
+
const products = [...carried, ...data.products];
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<>
|
|
375
|
+
<ul>
|
|
376
|
+
{products.map((product) => (
|
|
377
|
+
<li key={product.id}>{product.name}</li>
|
|
378
|
+
))}
|
|
379
|
+
</ul>
|
|
380
|
+
<Link
|
|
381
|
+
to={`/products?page=${data.page + 1}`}
|
|
382
|
+
state={[CarriedProducts(products)]}
|
|
383
|
+
scroll={false}
|
|
384
|
+
>
|
|
385
|
+
Load more
|
|
386
|
+
</Link>
|
|
387
|
+
</>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The list is a plain concatenation. After "Load more" the next page's loader
|
|
393
|
+
streams while the current page stays on screen, and `carried` stays the
|
|
394
|
+
current entry's until that page lands: the carried products and the loader's
|
|
395
|
+
page change in one commit, so no product is ever listed twice.
|
|
396
|
+
|
|
397
|
+
Without the option, a refresh of `?page=6` renders 50 products on the server
|
|
398
|
+
and then inserts the stored 250 above them. With it, the refreshed page stays
|
|
399
|
+
as the server rendered it:
|
|
400
|
+
|
|
401
|
+
| What happens to the entry | A `clearOnReload` slot |
|
|
402
|
+
| ------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
403
|
+
| The reader mounts during a client navigation (`<Link>`, `router.push`) | applied, as without the option |
|
|
404
|
+
| back/forward inside the running app (popstate), a server action's state | applied, as without the option |
|
|
405
|
+
| A document load: refresh, or a back/forward that loads the document | not applied, and removed from `history.state` |
|
|
406
|
+
| Later on that page: a reader mounts, or a navigation returns to the entry | still `undefined`; the next write (the next "Load more") stores anew |
|
|
407
|
+
| The page is restored from the browser's back/forward cache | untouched: nothing is loaded or hydrated |
|
|
408
|
+
|
|
409
|
+
- The rule is any document load of the entry, not only the Reload button:
|
|
410
|
+
restoring a closed tab or duplicating a tab loads the document too.
|
|
411
|
+
- Only the entry being loaded is cleared. Going back from it to an earlier
|
|
412
|
+
entry inside the running app is a client navigation and applies that
|
|
413
|
+
entry's state.
|
|
414
|
+
- The router removes every `~r` key when the client starts, before hydration.
|
|
415
|
+
It goes by the key alone, so it does not matter whether a reader is mounted,
|
|
416
|
+
where it sits (a `<Suspense>` boundary that hydrates late included), or
|
|
417
|
+
whether the definition's module is loaded at all. Other slots on the entry
|
|
418
|
+
are left alone.
|
|
419
|
+
- State the server sets is never dropped by this: a document response carries
|
|
420
|
+
no location state, and `ctx.setLocationState()` / `redirect(url, { state })`
|
|
421
|
+
reach `history.state` only through navigations and actions in the running
|
|
422
|
+
app, after start-up.
|
|
423
|
+
- `.write()` stores under the same key, so it does not survive a refresh
|
|
424
|
+
either.
|
|
425
|
+
- Adding the option to an existing definition moves it to the `~r` key: state
|
|
426
|
+
stored before, under the plain key, is no longer read (and is not removed).
|
|
427
|
+
Removing the option moves it back to the plain key; what was stored under
|
|
428
|
+
`~r` is removed at the next document load.
|
|
429
|
+
- It cannot be combined with `flash`: flash state is removed at its first
|
|
430
|
+
read, so the pair could only drop a message nobody has seen yet.
|
|
431
|
+
|
|
233
432
|
### .read() (non-hook access)
|
|
234
433
|
|
|
235
434
|
Read current location state outside React components (client-side only):
|
|
@@ -264,9 +463,11 @@ Static counterparts to `.read()`. Both mutate the current history entry's
|
|
|
264
463
|
bookkeeping, other location state slots). Both are client-only; they throw
|
|
265
464
|
when called on the server.
|
|
266
465
|
|
|
267
|
-
Neither
|
|
268
|
-
|
|
269
|
-
|
|
466
|
+
Neither changes what a `useLocationState` reader shows, mounted or mounting
|
|
467
|
+
later: readers get the entry's state as it was committed (see "When a reader
|
|
468
|
+
sees an entry's state"). The written value is there for `.read()` at once,
|
|
469
|
+
and for readers when the entry is next restored (back/forward to it, or a
|
|
470
|
+
reload).
|
|
270
471
|
|
|
271
472
|
```tsx
|
|
272
473
|
"use client";
|
|
@@ -275,7 +476,7 @@ import { ProductState } from "./state";
|
|
|
275
476
|
// Persisted across hard refresh and back/forward of this entry.
|
|
276
477
|
ProductState.write({ name: "Widget", price: 9.99 });
|
|
277
478
|
|
|
278
|
-
// Read later
|
|
479
|
+
// Read later.
|
|
279
480
|
const current = ProductState.read();
|
|
280
481
|
|
|
281
482
|
// Manually clear the slot. Idempotent if it isn't set.
|