@rangojs/router 0.31.0 → 0.33.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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
"node": ">=24.0.0"
|
|
260
260
|
},
|
|
261
261
|
"scripts": {
|
|
262
|
-
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --
|
|
262
|
+
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && rm -rf dist/bin && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --splitting --outdir=dist/bin --platform=node --packages=external && chmod +x dist/bin/rango.js && mkdir -p dist/bin/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/bin/plugins/cloudflare-protocol-loader-hook.mjs",
|
|
263
263
|
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
264
264
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
265
265
|
"test": "playwright test",
|
|
@@ -112,6 +112,14 @@ here for the rules they share.
|
|
|
112
112
|
keeps the visitor's URL. A loader, handler or server action
|
|
113
113
|
cannot rewrite, and a loop answers 508. Redirect when the new URL should
|
|
114
114
|
show (`/middleware`, "Rewrite").
|
|
115
|
+
9. **Keep prefetches shared.** One prefetch of a page serves every page
|
|
116
|
+
that links to it. A `revalidate()` that reads the page being left and
|
|
117
|
+
keeps its segment shares those prefetches only between the pages with
|
|
118
|
+
equal values of what it read (`currentParams.locale`: the pages of one
|
|
119
|
+
locale); one that reads `currentUrl` makes them page-only. Read params
|
|
120
|
+
by name, keep page-specific blocks in their route, and write such a rule
|
|
121
|
+
for chrome that shows a param's data
|
|
122
|
+
([Prefetches are shared](#prefetches-are-shared)).
|
|
115
123
|
|
|
116
124
|
## A small app: one module
|
|
117
125
|
|
|
@@ -217,6 +225,72 @@ name a route alike. One unnamed level in the root module is fine.
|
|
|
217
225
|
For where boundaries and `revalidate()` rules go inside that chrome so
|
|
218
226
|
navigations stay smooth, see the `ux-architecture` skill.
|
|
219
227
|
|
|
228
|
+
## Prefetches are shared
|
|
229
|
+
|
|
230
|
+
In production every visible `<Link>` prefetches its page, and one prefetch
|
|
231
|
+
of a page serves every page that links to it: a product prefetched on the
|
|
232
|
+
home page is not fetched again on the search page or on page 2 of a list
|
|
233
|
+
(`/links` → "Shared prefetches and `prefetchKey`"). A structure keeps that
|
|
234
|
+
sharing as long as no segment's answer depends on the page being left.
|
|
235
|
+
|
|
236
|
+
The default rules keep it: a standalone layout is held from every page that
|
|
237
|
+
shows it, and a route re-renders when its params or search change. A
|
|
238
|
+
`revalidate()` that reads the page being left and keeps its segment narrows
|
|
239
|
+
it to what the rule read. A rule that reads `currentParams` or
|
|
240
|
+
`fromRouteName` shares the answer between the pages with equal values of
|
|
241
|
+
those reads (the params a rule reads by name, every param when it spreads
|
|
242
|
+
`currentParams`, the route name for `fromRouteName`; the page's set of params
|
|
243
|
+
always counts). A rule that reads `currentUrl` makes it page-only: the server
|
|
244
|
+
stores the answer for the exact page that sent it, query string and hash
|
|
245
|
+
included, and each page prefetches those links again. The answer stays
|
|
246
|
+
correct; what is lost is the sharing.
|
|
247
|
+
|
|
248
|
+
| What you are placing | Put it in | Prefetches |
|
|
249
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| A block that belongs to one page (a category's hero, a product's breadcrumb) | that page's route: its handler, or a layout inside its `path()` (below) | Shared. It re-renders with its route on a param or search change. In a layout other pages share it would need a rule that reads the page being left |
|
|
251
|
+
| Links that follow a mount param (`:locale`, `:accountId`, `:projectId`) | a client component with `useReverse(routes)` (rule 7) | Shared. The links follow `useParams()`, and the layout stays held with no rule |
|
|
252
|
+
| Chrome that shows a param's data (the locale's messages, a project's name) | the layout, with a `revalidate()` comparing `currentParams` and `nextParams` | Shared between the pages with the same value of that param where the rule keeps the layout (the pages of one locale or one project) |
|
|
253
|
+
| A page whose answer depends on the page that sent it through something the server cannot see (`context` in a `revalidate()`, the `Referer` header) | `prefetchKey=":source"` on the links to it | Page-only, for those links |
|
|
254
|
+
|
|
255
|
+
The third row is the one to weigh: write that rule for chrome that must
|
|
256
|
+
follow the param, read the param by name, and keep everything else that
|
|
257
|
+
varies by page out of the layout.
|
|
258
|
+
|
|
259
|
+
Chrome that one kind of page has (a category's hero and breadcrumbs, a
|
|
260
|
+
product's gallery frame) can be a layout of its own route instead of a
|
|
261
|
+
template shared with other pages. A layout declared inside a `path()` belongs
|
|
262
|
+
to that route (`/layout`, "Orphan Layout"):
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
urls(({ path, layout }) => [
|
|
266
|
+
path("/c/:category", CategoryPage, { name: "category" }, () => [
|
|
267
|
+
layout(CategoryLayout), // the hero: re-renders with the category
|
|
268
|
+
]),
|
|
269
|
+
path("/p/:slug", ProductPage, { name: "product" }, () => [
|
|
270
|
+
layout(ProductLayout),
|
|
271
|
+
]),
|
|
272
|
+
]);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- **No rule, shared prefetches.** It re-renders on its route's param or
|
|
276
|
+
search change by default, so nothing reads the page being left.
|
|
277
|
+
- **It cannot leak.** The category's layout is not in the product page's
|
|
278
|
+
tree, so no navigation can keep it above a product page or the cart.
|
|
279
|
+
- **What it costs.** Its handler runs on every param or search change of its
|
|
280
|
+
route, `?page=2` included; cache an expensive read with `"use cache"`. Without
|
|
281
|
+
a `transition()` in its chain (the route, this layout or a layout above) it
|
|
282
|
+
remounts on a param change, like the route. A navigation to another route
|
|
283
|
+
mounts that route's layout: nothing is held between them.
|
|
284
|
+
- **In a `clientUrls()` group** a `path()` takes no `layout()`, only
|
|
285
|
+
`loader()`, `loading()` and `transition()` (`/client-urls`). A group
|
|
286
|
+
layout has no `revalidate()` of its own: it is a client component, so the
|
|
287
|
+
params it reads with `useParams()` are always the current page's. Its
|
|
288
|
+
loaders are held on navigation like a server layout's, though, so hero
|
|
289
|
+
data from a layout loader needs a rule that reads the page being left.
|
|
290
|
+
Put the block in the route component and its data in a loader on the
|
|
291
|
+
`path()`: that loader re-runs when the params or search change, with no
|
|
292
|
+
rule.
|
|
293
|
+
|
|
220
294
|
## Growing an app
|
|
221
295
|
|
|
222
296
|
1. **Start with one `urls()` module** ([A small app](#a-small-app-one-module)).
|
|
@@ -424,12 +424,13 @@ without JavaScript too (`/server-actions` → "Progressive Enhancement").
|
|
|
424
424
|
|
|
425
425
|
## Why
|
|
426
426
|
|
|
427
|
-
| Choice
|
|
428
|
-
|
|
|
429
|
-
| `ppr` on category and product pages
|
|
430
|
-
| `"use cache"` + `cacheTag()` for product data
|
|
431
|
-
| Live loaders for price, stock, cart
|
|
432
|
-
| `CartLoader` on the store layout
|
|
433
|
-
| `TRACKING_SEARCH_PARAMS` excluded
|
|
434
|
-
| `prefetch: false` on reviews
|
|
435
|
-
|
|
|
427
|
+
| Choice | Why |
|
|
428
|
+
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
429
|
+
| `ppr` on category and product pages | The shared part is HTML from the shell; only the holes render per request |
|
|
430
|
+
| `"use cache"` + `cacheTag()` for product data | One read for every visitor, and the tag reaches the shells that baked it |
|
|
431
|
+
| Live loaders for price, stock, cart | Fresh on every request, shell HITs included |
|
|
432
|
+
| `CartLoader` on the store layout | One badge on every page; held on navigations; narrowed to cart actions |
|
|
433
|
+
| `TRACKING_SEARCH_PARAMS` excluded | Campaign links do not multiply shells |
|
|
434
|
+
| `prefetch: false` on reviews | Every product card prefetches its page; few shoppers scroll to the reviews |
|
|
435
|
+
| Category content in `CategoryPage`, not a shared layout | A category hero belongs there too, or in a layout inside the category `path()`. In a layout shared with product pages it needs a `revalidate()` that reads the page being left, and every prefetch under that layout is shared only between the pages of one category ([Prefetches are shared](./SKILL.md#prefetches-are-shared)) |
|
|
436
|
+
| Checkout without `ppr` or `cache()` | Nothing on it is shared |
|
|
@@ -710,6 +710,10 @@ Rules of thumb:
|
|
|
710
710
|
keeps the page being left on screen until the fill answers.
|
|
711
711
|
- A same-route click (`/app/projects/1` to `/app/projects/2`) prefetches a
|
|
712
712
|
flagged loader as usual: only data the page does not have yet is deferred.
|
|
713
|
+
- One prefetch of a page serves every page that links to it. A `revalidate()`
|
|
714
|
+
that reads the page being left and keeps its segment shares it only between
|
|
715
|
+
the pages with equal values of what it read
|
|
716
|
+
([Prefetches are shared](./SKILL.md#prefetches-are-shared)).
|
|
713
717
|
|
|
714
718
|
Details: `/loader` → "`prefetch: false`", `/client-urls` → "`prefetch: false`".
|
|
715
719
|
|
|
@@ -802,11 +806,14 @@ reverse(".issues", { projectId: "api" }); // /acme/api/issues
|
|
|
802
806
|
|
|
803
807
|
- A link into another module (the project nav's "Account settings") is built
|
|
804
808
|
on the server and passed as a prop or loader data. `useReverse(accountRoutes)`
|
|
805
|
-
in a project component gives a wrong URL with no error
|
|
809
|
+
in a project component gives a wrong URL with no error.
|
|
806
810
|
- A server layout stays held when `:projectId` changes (partial rendering by
|
|
807
811
|
design), so links it rendered keep the old project. Render the project
|
|
808
812
|
switcher and nav in a client component with `useReverse(routes)`, or give
|
|
809
|
-
the layout a `revalidate()` comparing `projectId`.
|
|
813
|
+
the layout a `revalidate()` comparing `projectId`. Prefer the client
|
|
814
|
+
component: the rule reads the page being left, so a prefetch it keeps the
|
|
815
|
+
layout for is shared only between the pages of one project
|
|
816
|
+
([Prefetches are shared](./SKILL.md#prefetches-are-shared)).
|
|
810
817
|
- The account layout's `ctx.reverse(".settings")` resolves in the account
|
|
811
818
|
module on every page below it, the project's pages included: it links to
|
|
812
819
|
`/acme/settings`, not the project's settings route.
|
|
@@ -854,6 +861,12 @@ export function ProjectNav() {
|
|
|
854
861
|
}
|
|
855
862
|
```
|
|
856
863
|
|
|
864
|
+
`ProjectNavLoader`'s rule has the same cost: it reads `currentParams`, so a
|
|
865
|
+
prefetch from `/acme/web/issues` to `/acme/web/settings` is shared only by
|
|
866
|
+
the pages of the `web` project with the same set of params. Use it for links that need the server; links that
|
|
867
|
+
stay inside the project module can come from `useReverse(routes)` in
|
|
868
|
+
`ProjectNav`, with no rule.
|
|
869
|
+
|
|
857
870
|
## Why each piece sits where it does
|
|
858
871
|
|
|
859
872
|
| Piece | Where | Why |
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -1121,7 +1121,7 @@ results first, outermost first:
|
|
|
1121
1121
|
| deeper nesting | Composes the same way; the innermost boundary adds its default key without a `key` of its own, its request kind with one |
|
|
1122
1122
|
| under a `cache({ store })` on another store | Also that store's `keyGenerator` result, one part per store: empty for a store that returns the default key while another's differs, none when all return it |
|
|
1123
1123
|
| `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
|
|
1124
|
-
| no `key` anywhere | The default key, or the store's `keyGenerator` result,
|
|
1124
|
+
| no `key` anywhere | The default key, or the store's `keyGenerator` result, then the request kind when that result does not start with it |
|
|
1125
1125
|
|
|
1126
1126
|
- No `key()` result, whatever it returns, equals a default key or a
|
|
1127
1127
|
composed key, and no two composed keys are equal: a namespaced result
|
|
@@ -1150,10 +1150,15 @@ results first, outermost first:
|
|
|
1150
1150
|
empty result there fails key resolution, so the request renders uncached,
|
|
1151
1151
|
and the router warns once naming the store. On the same store the inner
|
|
1152
1152
|
boundary's own default key already carries it (or its own `key()`
|
|
1153
|
-
overrides it).
|
|
1154
|
-
|
|
1155
|
-
result
|
|
1156
|
-
|
|
1153
|
+
overrides it). A `keyGenerator` result keeps the request kind: the
|
|
1154
|
+
default key starts with it (`doc:`, `partial:`, `intercept:<intercept>:`),
|
|
1155
|
+
and a result that does not gets it as a last part (`items` is stored as
|
|
1156
|
+
`items|doc` for a document request), so a document request, a navigation
|
|
1157
|
+
and each intercept navigation keep their own record whatever it returns.
|
|
1158
|
+
Build on the default key (`` `${defaultKey}|${locale}` ``) and the result
|
|
1159
|
+
is the key, byte for byte. A prefix before it
|
|
1160
|
+
(`` `${locale}:${defaultKey}` ``) keeps the kind too, but no longer starts
|
|
1161
|
+
with it, so the kind is appended (`en%3Adoc%3A…|doc`).
|
|
1157
1162
|
- A response route's entry (`path.json()` and the other response routes) is
|
|
1158
1163
|
keyed the same way, behind a `response:` prefix. A `ppr` route's shell is
|
|
1159
1164
|
partitioned by the chain's `key()` results, namespaced the same way, since
|
|
@@ -278,7 +278,11 @@ the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
|
|
|
278
278
|
`actionUrl`, `actionResult`, `formData`, `method`,
|
|
279
279
|
`routeName`/`fromRouteName`/`toRouteName`, and `stale`. Nothing runs in the
|
|
280
280
|
browser before the request goes out; there is no client-computed decision and
|
|
281
|
-
no hybrid.
|
|
281
|
+
no hybrid. A predicate that reads `currentParams` or `fromRouteName` and
|
|
282
|
+
keeps its segment shares a prefetch of that answer only between the pages
|
|
283
|
+
with equal values of what it read, and one that reads `currentUrl` makes it
|
|
284
|
+
page-only, as in `urls()` (`/links` → "Shared prefetches and
|
|
285
|
+
`prefetchKey`").
|
|
282
286
|
|
|
283
287
|
You still write the predicate in the `"use client"` module, or in a
|
|
284
288
|
component's `.use`. The rango Vite plugin hoists each `revalidate()` argument
|
|
@@ -400,7 +404,7 @@ Everything after that is decided on the server, while it handles that request:
|
|
|
400
404
|
the action's `actionResult` and `formData` (above). A loader that holds keeps
|
|
401
405
|
the data the page shows.
|
|
402
406
|
- **The browser's caches.** When the response arrives the browser marks its
|
|
403
|
-
history cache
|
|
407
|
+
history cache and its prefetches stale, unless the action called
|
|
404
408
|
`keepClientCache()` (`/server-actions` → "Client Cache After an Action").
|
|
405
409
|
- **A loader that fails on its rerun** reaches your `onError` on the server,
|
|
406
410
|
and in the browser it throws at its read, below.
|
|
@@ -619,21 +623,23 @@ shell, such as a `useActionState` form (React suspends it once to encode its
|
|
|
619
623
|
action), is in the first flush too: the router gives React up to three more
|
|
620
624
|
turns before it, and none when the document is already complete.
|
|
621
625
|
|
|
622
|
-
## `prefetch: false` — keep a loader out of prefetches
|
|
626
|
+
## `prefetch: false` — keep a loader out of speculative prefetches
|
|
623
627
|
|
|
624
628
|
A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
|
|
625
|
-
and
|
|
629
|
+
and a viewport or render prefetch skips it. A hover prefetch, and
|
|
630
|
+
`useRouter().prefetch()`, is intent to click: it runs the loader, and the
|
|
631
|
+
click sends no fill (`/loader` → "`prefetch: false`").
|
|
626
632
|
|
|
627
633
|
```tsx
|
|
628
634
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
629
635
|
loader(PriceLoader),
|
|
630
|
-
loader(ReviewsLoader, { prefetch: false }), // kept out of prefetches
|
|
636
|
+
loader(ReviewsLoader, { prefetch: false }), // kept out of viewport prefetches
|
|
631
637
|
loading(<ProductSkeleton />),
|
|
632
638
|
]),
|
|
633
639
|
```
|
|
634
640
|
|
|
635
|
-
A click inside the group still shows the destination at once. With
|
|
636
|
-
prefetch received, the URL and the other loaders' data move with the click
|
|
641
|
+
A click inside the group still shows the destination at once. With a
|
|
642
|
+
viewport or render prefetch received, the URL and the other loaders' data move with the click
|
|
637
643
|
too, `useOutlet().pending` ends with that commit, and one follow-up request
|
|
638
644
|
(the fill) runs only the flagged loaders. Read the flagged loader inside its
|
|
639
645
|
own `<Suspense>` to show the page and that fallback at once; with no boundary
|
|
@@ -654,10 +660,11 @@ rest of the destination; otherwise it is held. A same-route click
|
|
|
654
660
|
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
655
661
|
on a server layout above the group defers nothing for the group's routes.
|
|
656
662
|
- On an `intercept()` loader the flag works as in `urls()`: an in-group
|
|
657
|
-
prefetch of the intercept's target defers it, and the
|
|
658
|
-
at once with one fill for it.
|
|
659
|
-
- With `ssr: false` on a `ppr` group route the flag
|
|
660
|
-
|
|
663
|
+
viewport or render prefetch of the intercept's target defers it, and the
|
|
664
|
+
click opens the modal at once with one fill for it; a hover's runs it.
|
|
665
|
+
- With `ssr: false` on a `ppr` group route the flag works as on a server
|
|
666
|
+
route: deferred unless the prefetch serves the loader from a pin of a
|
|
667
|
+
document-captured shell (`/ppr` → "`prefetch: false` on a `ppr` route").
|
|
661
668
|
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
662
669
|
itself, and `rangoUseClientTransform()` installs the group's projection
|
|
663
670
|
(`/testing` → `serve-shell-request.md`, "`clientUrls()` routes"). It also
|
|
@@ -558,8 +558,9 @@ analog but operates on loader/query reload, not RSC-segment render.
|
|
|
558
558
|
|
|
559
559
|
`<Link prefetch="hover|viewport|render|adaptive|none">` (default: the router's
|
|
560
560
|
`defaultPrefetch`, `"none"` in development and `"viewport"` in production), plus
|
|
561
|
-
`prefetchKey` (`":source"` scopes a prefetch to the originating page
|
|
562
|
-
|
|
561
|
+
`prefetchKey` (`":source"` scopes a prefetch to the originating page; the
|
|
562
|
+
router does so itself for an intercept target and for a `revalidate()` that
|
|
563
|
+
read the source page and kept a segment). Intercepted plain `<a href>` elements
|
|
563
564
|
inside the router basename follow the same default; common static-resource
|
|
564
565
|
extensions are excluded unless `data-prefetch="true"` identifies an application
|
|
565
566
|
route, and `data-prefetch="false"` or `"none"` opts out unsafe GET links. A
|
|
@@ -582,9 +583,11 @@ the bounded cache uses a configurable TTL. `Save-Data`,
|
|
|
582
583
|
`prefers-reduced-data`, or `prefetchCacheTTL: false` disable speculation.
|
|
583
584
|
|
|
584
585
|
Correctness is part of the prefetch contract. Keys include Rango state, destination
|
|
585
|
-
URL, and the mounted segment set used to compute the diff. Server actions
|
|
586
|
-
|
|
587
|
-
|
|
586
|
+
URL, and the mounted segment set used to compute the diff. Server actions mark
|
|
587
|
+
every prefetch stale without aborting it: a click on a stale one shows it at once,
|
|
588
|
+
then revalidates the page in the background. `invalidateClientCache()` rotates
|
|
589
|
+
state and aborts speculative work; a generation check prevents a late result from
|
|
590
|
+
repopulating a flushed cache. Routes an intercept targets are automatically
|
|
588
591
|
source-scoped, matched or not,
|
|
589
592
|
and malformed, cross-origin, foreign-router, redirect, and reload responses are
|
|
590
593
|
dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owning
|
|
@@ -594,7 +597,7 @@ link.
|
|
|
594
597
|
session-cookie value shaped as `{documentVersion}:{invalidationTimestamp}`. Navigation
|
|
595
598
|
and prefetch requests send it as `X-Rango-State`, and RSC responses vary on that
|
|
596
599
|
header, so the browser HTTP cache and Rango's decoded prefetch map share one cache
|
|
597
|
-
identity. A deployment that changes the router's code or client assets changes the version;
|
|
600
|
+
identity. A deployment that changes the router's code or client assets changes the version; `invalidateClientCache()` rotates the timestamp. Old
|
|
598
601
|
responses become unreachable under the retired identity instead of requiring every
|
|
599
602
|
cache layer to delete the same entries successfully.
|
|
600
603
|
|
|
@@ -608,10 +611,13 @@ cookie. Across those two seats, it covers mutations initiated outside the action
|
|
|
608
611
|
bridge: REST calls, client-observed WebSocket events, authentication changes, or
|
|
609
612
|
middleware/loader decisions.
|
|
610
613
|
|
|
611
|
-
Server Actions
|
|
612
|
-
|
|
613
|
-
the
|
|
614
|
-
|
|
614
|
+
Server Actions mark the history and prefetch caches stale automatically after a
|
|
615
|
+
response reaches the browser, without rotating the state: the response bumps
|
|
616
|
+
only its write part, so the browser HTTP cache misses on responses from before
|
|
617
|
+
the write while the in-memory caches go stale. If an action knows it
|
|
618
|
+
changed nothing that any route renders, `keepClientCache()` is the explicit
|
|
619
|
+
per-response escape hatch: it suppresses that stale-marking, the broadcast, and
|
|
620
|
+
the revalidation fetch. Together,
|
|
615
621
|
`invalidateClientCache()` and `keepClientCache()` give application code control in
|
|
616
622
|
both directions — force a miss for mutations the router cannot observe, or preserve
|
|
617
623
|
the current state for a known no-op.
|
|
@@ -217,6 +217,14 @@ suffixes are still appended.
|
|
|
217
217
|
request that holds no segments (`router.refresh()`, a cross-tab refresh).
|
|
218
218
|
Each renders at the origin and is not stored, because its data may predate
|
|
219
219
|
a write and the cache cannot tell.
|
|
220
|
+
- A client navigation whose answer depends on the page it came from: a
|
|
221
|
+
`revalidate()` rule that read `currentParams`, `currentUrl` or
|
|
222
|
+
`fromRouteName` kept a segment, or an intercept targets the route. The key
|
|
223
|
+
names the segments the client holds, not its page, so `/c/tv` would get the
|
|
224
|
+
answer `/c/audio` got for `/c/audio?sort=x` and keep its own hero. It is sent
|
|
225
|
+
`private, no-cache` over the route's `Cache-Control` and is not stored. One
|
|
226
|
+
whose rule re-rendered the segment is right from every page and is stored as
|
|
227
|
+
usual.
|
|
220
228
|
- Non-GET requests
|
|
221
229
|
- Responses without an `s-maxage` `Cache-Control` directive
|
|
222
230
|
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
@@ -228,6 +236,11 @@ suffixes are still appended.
|
|
|
228
236
|
(`rango-state-fresh` by default), so it is not stored. A stale entry's
|
|
229
237
|
background re-run sets no fresh-reads cookie (no visitor made that
|
|
230
238
|
invalidation), so its invalidation does not keep the refreshed page out.
|
|
239
|
+
- A document that carries location state (`ctx.setLocationState()`, or the
|
|
240
|
+
one-shot cookie a document redirect left for it): it is the visitor's, so it
|
|
241
|
+
is not stored, leaves `private, no-store`, and a request that arrives with
|
|
242
|
+
that cookie, or whose earlier middleware set state, is not answered from a
|
|
243
|
+
stored entry (see `/hooks` `state.md`, "State on a document response")
|
|
231
244
|
- Non-200 responses
|
|
232
245
|
- A 200 whose render reported an error: a component that threw after the
|
|
233
246
|
response started streaming, such as an async server component whose fetch
|
|
@@ -160,14 +160,14 @@ function NavigationControls() {
|
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
| Method | Behaviour
|
|
164
|
-
| ------------------------ |
|
|
165
|
-
| `push(url, options?)` | Navigate and add a history entry. Returns a promise.
|
|
166
|
-
| `replace(url, options?)` | Navigate and replace the current history entry. Returns a promise.
|
|
167
|
-
| `refresh()` | Re-fetch the current route's server data, keeping client state. Returns a promise.
|
|
168
|
-
| `prefetch(url, opts?)` | Prefetch a target. `{ key: ":source" }` scopes the entry to the source page (parity with `<Link prefetchKey
|
|
169
|
-
| `back()` | `history.back()`; on the first entry of the session it replaces to the app root instead of leaving the app.
|
|
170
|
-
| `forward()` | `history.forward()`.
|
|
163
|
+
| Method | Behaviour |
|
|
164
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `push(url, options?)` | Navigate and add a history entry. Returns a promise. |
|
|
166
|
+
| `replace(url, options?)` | Navigate and replace the current history entry. Returns a promise. |
|
|
167
|
+
| `refresh()` | Re-fetch the current route's server data, keeping client state. Returns a promise. |
|
|
168
|
+
| `prefetch(url, opts?)` | Prefetch a target. `{ key: ":source" }` scopes the entry to the source page (parity with `<Link prefetchKey>`, see `/links`). |
|
|
169
|
+
| `back()` | `history.back()`; on the first entry of the session it replaces to the app root instead of leaving the app. |
|
|
170
|
+
| `forward()` | `history.forward()`. |
|
|
171
171
|
|
|
172
172
|
`push` / `replace` options: `scroll` (`false` keeps the current scroll
|
|
173
173
|
position), `revalidate` (default `true`, see below), `transition` (`false`
|
package/skills/hooks/state.md
CHANGED
|
@@ -101,6 +101,7 @@ Back/forward and state the server sets on a navigation
|
|
|
101
101
|
| `<Link state>`, `router.push()` / `router.replace()` | in the commit that shows the destination |
|
|
102
102
|
| back/forward | in the commit that restores the entry (cached or refetched) |
|
|
103
103
|
| the server, on a navigation (`setLocationState`, a redirect) | in the commit that shows the destination |
|
|
104
|
+
| the server, on a document load (a page load, a no-JS post) | once the page has hydrated, as stored state |
|
|
104
105
|
| a navigation the server answers with nothing to re-render | with the new location: the page on screen is the entry's |
|
|
105
106
|
| `router.push(url, { revalidate: false })` (no server fetch) | with the new location: the page does not change |
|
|
106
107
|
| a server action's `setLocationState` (no navigation) | when the action's response arrives, added to what readers show |
|
|
@@ -279,8 +280,8 @@ import { FlashMessage } from "../location-states";
|
|
|
279
280
|
```
|
|
280
281
|
|
|
281
282
|
Or via `ctx.setLocationState()` from a handler or middleware. It is delivered
|
|
282
|
-
with a client navigation's or an action's response
|
|
283
|
-
|
|
283
|
+
with a client navigation's or an action's response, and with a document
|
|
284
|
+
response too (see "State on a document response" below):
|
|
284
285
|
|
|
285
286
|
```tsx
|
|
286
287
|
(ctx) => {
|
|
@@ -289,6 +290,121 @@ carries no location state (there is no history entry to write it to yet):
|
|
|
289
290
|
};
|
|
290
291
|
```
|
|
291
292
|
|
|
293
|
+
### Several writes to one definition
|
|
294
|
+
|
|
295
|
+
Entries accumulate across definitions, but one definition is one slot in
|
|
296
|
+
`history.state`: for the same definition the last write wins. Two
|
|
297
|
+
`ctx.setLocationState(Toasts(...))` calls in one response, or
|
|
298
|
+
`redirect(url, { state: [Toasts(a), Toasts(b)] })`, deliver only the later
|
|
299
|
+
value, without a warning. A server action, a handler and the middleware
|
|
300
|
+
around them all write to the same response, so a shared helper that writes a
|
|
301
|
+
definition replaces what an earlier call wrote.
|
|
302
|
+
|
|
303
|
+
To combine several messages, make the state an array and write it once, for
|
|
304
|
+
example by collecting them in a request variable and writing the definition
|
|
305
|
+
at the end of the action:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
export const Toasts = createLocationState<ToastMessage[]>({ flash: true });
|
|
309
|
+
|
|
310
|
+
// One write per response, with every toast in it.
|
|
311
|
+
getRequestContext().setLocationState(Toasts([...savedToasts, loginToast]));
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### State on a document response
|
|
315
|
+
|
|
316
|
+
A document request has no history entry yet, so the router carries what the
|
|
317
|
+
server set to the client and writes it there when the client starts. This
|
|
318
|
+
covers the mutations that most need a flash message: a `<form action={fn}>`
|
|
319
|
+
posted before hydration or without JS, a payment provider redirecting the
|
|
320
|
+
shopper back, a link from an email.
|
|
321
|
+
|
|
322
|
+
| The document request | Where the state travels |
|
|
323
|
+
| ---------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
324
|
+
| renders a page (`setLocationState` in a handler, middleware or no-JS action) | the document's bootstrap payload |
|
|
325
|
+
| redirects (`redirect(url, { state })`, or `setLocationState` and a redirect) | a one-shot cookie the target's document request reads and clears |
|
|
326
|
+
| redirects with `{ external: true }` or off-origin | nowhere: the cookie would not reach the target |
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
// A payment provider sends the shopper back to /checkout/callback.
|
|
330
|
+
path("/checkout/callback", async (ctx) => {
|
|
331
|
+
const ok = await completeOrder(ctx.searchParams.get("token"));
|
|
332
|
+
return ok
|
|
333
|
+
? redirect("/orders/confirmed", { state: [Flash({ text: "Paid" })] })
|
|
334
|
+
: redirect("/checkout", { state: [Flash({ text: "Payment declined" })] });
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- **When a reader gets it.** `useLocationState` returns `undefined` while the
|
|
339
|
+
document streams and hydrates, then the value, at the moment it would get
|
|
340
|
+
state stored in the entry. A flash message shows just after hydration.
|
|
341
|
+
- **Without JS, no flash.** The state rides to the client, and only the
|
|
342
|
+
client writes it into the entry and hands it to readers. A visitor with JS
|
|
343
|
+
disabled gets the page as the server rendered it, where every reader of the
|
|
344
|
+
state shows `undefined`: the post and its redirect work, but the message
|
|
345
|
+
never shows. If such visitors must see it, render it from the server, for
|
|
346
|
+
example from a search param or a cookie your handler reads.
|
|
347
|
+
- **Flash and refresh.** Flash state is cleared once read, as always. The
|
|
348
|
+
cookie is cleared by the response that read it, so a refresh of the target
|
|
349
|
+
renders without it. A page whose handler sets state on every request
|
|
350
|
+
delivers it again on a refresh, because the handler ran again. A no-JS post
|
|
351
|
+
answered with a page (no redirect) is re-posted by the browser on refresh;
|
|
352
|
+
redirect after the post (`redirect(url, { status: 303, state })`) to avoid
|
|
353
|
+
that.
|
|
354
|
+
- **Caching.** A document that carries state, and the redirect carrying it
|
|
355
|
+
on, are sent `Cache-Control: private, no-store`, after every middleware.
|
|
356
|
+
The document cache neither stores such a document nor answers a request
|
|
357
|
+
that arrives with the cookie from a stored one. A shared PPR shell never
|
|
358
|
+
holds state: on a `ppr` route, state set before the render (by middleware,
|
|
359
|
+
or the cookie) rides the live payload of a shell HIT or MISS, and the shell
|
|
360
|
+
is still served and captured; state a handler sets keeps that route from
|
|
361
|
+
being captured, so it renders in full. `no-store` also keeps the
|
|
362
|
+
page out of the browser's back/forward cache (Chrome's bfcache), so Back to
|
|
363
|
+
it loads it again. That costs little for a one-off flash; a handler that
|
|
364
|
+
sets state on every request pays it on every page view.
|
|
365
|
+
- **`ppr` and cached routes.** A shell HIT and a `cache()` hit run no
|
|
366
|
+
handler, so a handler there cannot set state on that request. Set it from
|
|
367
|
+
middleware, which runs on every request; the HIT still serves, with the
|
|
368
|
+
state in its live payload. A HIT sends exactly the state set before its
|
|
369
|
+
render: its headers leave before its tail runs, so they decide then
|
|
370
|
+
whether the response is private.
|
|
371
|
+
- **Not from a loader.** A loader has no `setLocationState`, and calling
|
|
372
|
+
`getRequestContext().setLocationState()` from one is not supported: the
|
|
373
|
+
payload takes the request's state before loaders settle, so a loader's
|
|
374
|
+
write may arrive (a `ppr` MISS then renders in full) or be dropped, and a
|
|
375
|
+
shell HIT never sends it. Set state from middleware, the handler or an
|
|
376
|
+
action.
|
|
377
|
+
- **The cookie.** It is named after the router's state cookie
|
|
378
|
+
(`<stateCookieName>-ls`, `__Host-` prefixed on https), `HttpOnly`,
|
|
379
|
+
`SameSite=Lax`, `Path=/`, and lives 60 seconds. Only the next GET document
|
|
380
|
+
request of the redirect's target pathname that navigates reads it: where
|
|
381
|
+
the browser sends `Sec-Fetch-Dest`, it must be `document`, `iframe` or
|
|
382
|
+
`frame`, so a `<link rel=prefetch>` or a `fetch()` of that path leaves it
|
|
383
|
+
to the navigation. A form POST to it, a client navigation and a Flight
|
|
384
|
+
fetch leave it too, as does a request of another path. A
|
|
385
|
+
target that redirects again carries the state on to the next target. There
|
|
386
|
+
is one such cookie per router: two document redirects within a moment (two
|
|
387
|
+
tabs) keep only the later one's state. Without https there is no `__Host-`
|
|
388
|
+
prefix, so a sibling subdomain could set the cookie: treat state that
|
|
389
|
+
arrives this way as a message to show, not as a fact to act on. A `ppr`
|
|
390
|
+
shell HIT that read the cookie and then cannot finish its tail reloads the
|
|
391
|
+
page into a full render; its headers already cleared the cookie, so its
|
|
392
|
+
reload script sets it again first (from script, so without `HttpOnly`,
|
|
393
|
+
for the moment the reload takes), and the reload shows the state.
|
|
394
|
+
- **Known limit: a redirect from one router to another.** The cookie is
|
|
395
|
+
named after the redirecting router's state cookie, which holds its router
|
|
396
|
+
id (`<stateCookiePrefix>_<routerId>`), and a router reads only its own. On
|
|
397
|
+
one origin served by several routers (a host router, or several
|
|
398
|
+
`createRouter()` apps behind one worker), a document redirect from one
|
|
399
|
+
router to a path another serves loses its state, without a warning. Carry
|
|
400
|
+
such a message another way, such as a search param.
|
|
401
|
+
- **JSON only across a redirect.** The cookie holds JSON: a `Date` arrives as
|
|
402
|
+
a string, a `Map` or `Set` as `{}`, `undefined` is dropped. Development
|
|
403
|
+
warns, naming the slot. A value JSON cannot encode (a `bigint`), or state
|
|
404
|
+
over the cookie's 4000-byte limit, is dropped from the redirect with a
|
|
405
|
+
`console.warn`. Carry an id and load the rest on the target page.
|
|
406
|
+
Navigation and action payloads are not limited this way.
|
|
407
|
+
|
|
292
408
|
### State from another deploy
|
|
293
409
|
|
|
294
410
|
History entries survive reloads and back/forward, so a tab left open across a
|
|
@@ -419,10 +535,10 @@ as the server rendered it:
|
|
|
419
535
|
where it sits (a `<Suspense>` boundary that hydrates late included), or
|
|
420
536
|
whether the definition's module is loaded at all. Other slots on the entry
|
|
421
537
|
are left alone.
|
|
422
|
-
- State the server sets is never dropped by this: a document
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
538
|
+
- State the server sets is never dropped by this: the state a document
|
|
539
|
+
response carries is written after the `~r` keys are removed, so a
|
|
540
|
+
`clearOnReload` slot the server set for this load is applied on this load
|
|
541
|
+
(and removed by the next one). Navigations and actions write after start-up.
|
|
426
542
|
- `.write()` stores under the same key, so it does not survive a refresh
|
|
427
543
|
either.
|
|
428
544
|
- Adding the option to an existing definition moves it to the `~r` key: state
|
|
@@ -503,9 +619,12 @@ from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
|
503
619
|
it is selected by export conditions:
|
|
504
620
|
|
|
505
621
|
- **Client** (client component or module): marks the history cache stale,
|
|
506
|
-
flushes the prefetch cache, rotates the router state, and notifies other
|
|
507
|
-
|
|
508
|
-
|
|
622
|
+
flushes the prefetch cache, rotates the router state, and notifies other
|
|
623
|
+
tabs, which adopt the new state and flush theirs. That is more than a
|
|
624
|
+
completed server action does: an action marks history and prefetches stale
|
|
625
|
+
and rotates nothing (its response bumps only the state's write part, so the
|
|
626
|
+
browser HTTP cache misses on responses from before the write). During SSR
|
|
627
|
+
of a client component it is a no-op (dev warning).
|
|
509
628
|
- **Server** (handler, server component, middleware, loader, or action): writes
|
|
510
629
|
a rotated state `Set-Cookie` for the responding client, which marks its
|
|
511
630
|
caches stale when it next reads them. Idempotent within a request; throws
|
|
@@ -514,9 +633,10 @@ it is selected by export conditions:
|
|
|
514
633
|
from handler code writes the cookie only on the renders that run it (a
|
|
515
634
|
MISS); call it from middleware, which runs on every request, HITs included.
|
|
516
635
|
|
|
517
|
-
Server actions already
|
|
518
|
-
action, call `keepClientCache()` inside it
|
|
519
|
-
|
|
636
|
+
Server actions already mark the caches stale automatically; to suppress that
|
|
637
|
+
for a no-op action, call `keepClientCache()` inside it, and for the full flush
|
|
638
|
+
and rotation, call `invalidateClientCache()` inside it (see `/server-actions` →
|
|
639
|
+
"Client Cache After an Action").
|
|
520
640
|
|
|
521
641
|
```tsx
|
|
522
642
|
"use client";
|