@rangojs/router 0.32.0 → 0.33.1
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 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.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/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 +15 -6
- 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/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/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 +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -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 +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -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 +26 -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 +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- 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/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 +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- 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/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 +27 -0
- package/src/cache/document-cache.ts +5 -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/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 +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- 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 +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- 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 +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
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,21 @@ 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)).
|
|
123
|
+
10. **A layout keeps its `<Outlet />` in one place.** Branch inside one shape
|
|
124
|
+
(`<>{promo ? <Banner /> : null}<Outlet /></>`), never between a bare
|
|
125
|
+
`return <Outlet />` and a wrapped one: the outlet moves, and every page
|
|
126
|
+
below remounts when the branch flips, its state reset and its mount
|
|
127
|
+
effects run again. When an early return is unavoidable and the other
|
|
128
|
+
branch is only a fragment, give the outlet the same `key` in both
|
|
129
|
+
(`/layout`, "Moving the Outlet remounts its children").
|
|
115
130
|
|
|
116
131
|
## A small app: one module
|
|
117
132
|
|
|
@@ -217,6 +232,72 @@ name a route alike. One unnamed level in the root module is fine.
|
|
|
217
232
|
For where boundaries and `revalidate()` rules go inside that chrome so
|
|
218
233
|
navigations stay smooth, see the `ux-architecture` skill.
|
|
219
234
|
|
|
235
|
+
## Prefetches are shared
|
|
236
|
+
|
|
237
|
+
In production every visible `<Link>` prefetches its page, and one prefetch
|
|
238
|
+
of a page serves every page that links to it: a product prefetched on the
|
|
239
|
+
home page is not fetched again on the search page or on page 2 of a list
|
|
240
|
+
(`/links` → "Shared prefetches and `prefetchKey`"). A structure keeps that
|
|
241
|
+
sharing as long as no segment's answer depends on the page being left.
|
|
242
|
+
|
|
243
|
+
The default rules keep it: a standalone layout is held from every page that
|
|
244
|
+
shows it, and a route re-renders when its params or search change. A
|
|
245
|
+
`revalidate()` that reads the page being left and keeps its segment narrows
|
|
246
|
+
it to what the rule read. A rule that reads `currentParams` or
|
|
247
|
+
`fromRouteName` shares the answer between the pages with equal values of
|
|
248
|
+
those reads (the params a rule reads by name, every param when it spreads
|
|
249
|
+
`currentParams`, the route name for `fromRouteName`; the page's set of params
|
|
250
|
+
always counts). A rule that reads `currentUrl` makes it page-only: the server
|
|
251
|
+
stores the answer for the exact page that sent it, query string and hash
|
|
252
|
+
included, and each page prefetches those links again. The answer stays
|
|
253
|
+
correct; what is lost is the sharing.
|
|
254
|
+
|
|
255
|
+
| What you are placing | Put it in | Prefetches |
|
|
256
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
257
|
+
| 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 |
|
|
258
|
+
| 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 |
|
|
259
|
+
| 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) |
|
|
260
|
+
| 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 |
|
|
261
|
+
|
|
262
|
+
The third row is the one to weigh: write that rule for chrome that must
|
|
263
|
+
follow the param, read the param by name, and keep everything else that
|
|
264
|
+
varies by page out of the layout.
|
|
265
|
+
|
|
266
|
+
Chrome that one kind of page has (a category's hero and breadcrumbs, a
|
|
267
|
+
product's gallery frame) can be a layout of its own route instead of a
|
|
268
|
+
template shared with other pages. A layout declared inside a `path()` belongs
|
|
269
|
+
to that route (`/layout`, "Orphan Layout"):
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
urls(({ path, layout }) => [
|
|
273
|
+
path("/c/:category", CategoryPage, { name: "category" }, () => [
|
|
274
|
+
layout(CategoryLayout), // the hero: re-renders with the category
|
|
275
|
+
]),
|
|
276
|
+
path("/p/:slug", ProductPage, { name: "product" }, () => [
|
|
277
|
+
layout(ProductLayout),
|
|
278
|
+
]),
|
|
279
|
+
]);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- **No rule, shared prefetches.** It re-renders on its route's param or
|
|
283
|
+
search change by default, so nothing reads the page being left.
|
|
284
|
+
- **It cannot leak.** The category's layout is not in the product page's
|
|
285
|
+
tree, so no navigation can keep it above a product page or the cart.
|
|
286
|
+
- **What it costs.** Its handler runs on every param or search change of its
|
|
287
|
+
route, `?page=2` included; cache an expensive read with `"use cache"`. Without
|
|
288
|
+
a `transition()` in its chain (the route, this layout or a layout above) it
|
|
289
|
+
remounts on a param change, like the route. A navigation to another route
|
|
290
|
+
mounts that route's layout: nothing is held between them.
|
|
291
|
+
- **In a `clientUrls()` group** a `path()` takes no `layout()`, only
|
|
292
|
+
`loader()`, `loading()` and `transition()` (`/client-urls`). A group
|
|
293
|
+
layout has no `revalidate()` of its own: it is a client component, so the
|
|
294
|
+
params it reads with `useParams()` are always the current page's. Its
|
|
295
|
+
loaders are held on navigation like a server layout's, though, so hero
|
|
296
|
+
data from a layout loader needs a rule that reads the page being left.
|
|
297
|
+
Put the block in the route component and its data in a loader on the
|
|
298
|
+
`path()`: that loader re-runs when the params or search change, with no
|
|
299
|
+
rule.
|
|
300
|
+
|
|
220
301
|
## Growing an app
|
|
221
302
|
|
|
222
303
|
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 |
|
|
@@ -880,6 +893,9 @@ export function ProjectNav() {
|
|
|
880
893
|
user's name): the capture is refused and the page never serves a shell.
|
|
881
894
|
- **`prefetch: false` on every loader**: each click then waits for a fill.
|
|
882
895
|
Flag the expensive, rarely-clicked ones.
|
|
896
|
+
- **A layout that moves its `<Outlet />` between branches** (a bare
|
|
897
|
+
`return <Outlet />` on one, a wrapped one on another): every page below it
|
|
898
|
+
remounts when the branch flips. Keep one shape (`/layout`).
|
|
883
899
|
- **A bare `isAction(refs)` predicate**: it is a hard `false` on navigation
|
|
884
900
|
too, so the loader stops following param changes. Use
|
|
885
901
|
`isAction() ? isAction(refs) : undefined` (`/loader`).
|
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
|
|
@@ -738,6 +745,17 @@ a hard load of the target URL renders the full route.
|
|
|
738
745
|
throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
|
|
739
746
|
(route-independent lane; `revalidate()` deliberately not consulted).
|
|
740
747
|
Pinned in the router repo's client-urls hook-probe e2e.
|
|
748
|
+
- **An action called on the optimistic frame runs against the committed
|
|
749
|
+
page.** The destination mounts at the click, before the navigation
|
|
750
|
+
commits, so an action its mount effect calls is sent from the page the URL
|
|
751
|
+
bar still shows: the server runs it with that page's request context
|
|
752
|
+
(`ctx.url`, `ctx.params`, its middleware) and re-renders that page, not
|
|
753
|
+
the destination. Nothing of that render reaches the destination, and a
|
|
754
|
+
write still refreshes the destination once it commits (unless the action
|
|
755
|
+
called `keepClientCache()`). Pass what the action needs as arguments
|
|
756
|
+
instead of reading the request's URL or params. A link prefetched before
|
|
757
|
+
the click (the production default, `viewport`) usually commits before the
|
|
758
|
+
effect runs.
|
|
741
759
|
- **Location state follows the same split as the route hooks.** Inside the
|
|
742
760
|
optimistic branch (the destination and the group's client layouts)
|
|
743
761
|
`useLocationState` reads the state the navigation carries (`<Link state>`,
|
|
@@ -583,9 +583,11 @@ the bounded cache uses a configurable TTL. `Save-Data`,
|
|
|
583
583
|
`prefers-reduced-data`, or `prefetchCacheTTL: false` disable speculation.
|
|
584
584
|
|
|
585
585
|
Correctness is part of the prefetch contract. Keys include Rango state, destination
|
|
586
|
-
URL, and the mounted segment set used to compute the diff. Server actions
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
589
591
|
source-scoped, matched or not,
|
|
590
592
|
and malformed, cross-origin, foreign-router, redirect, and reload responses are
|
|
591
593
|
dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owning
|
|
@@ -595,7 +597,7 @@ link.
|
|
|
595
597
|
session-cookie value shaped as `{documentVersion}:{invalidationTimestamp}`. Navigation
|
|
596
598
|
and prefetch requests send it as `X-Rango-State`, and RSC responses vary on that
|
|
597
599
|
header, so the browser HTTP cache and Rango's decoded prefetch map share one cache
|
|
598
|
-
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
|
|
599
601
|
responses become unreachable under the retired identity instead of requiring every
|
|
600
602
|
cache layer to delete the same entries successfully.
|
|
601
603
|
|
|
@@ -609,10 +611,13 @@ cookie. Across those two seats, it covers mutations initiated outside the action
|
|
|
609
611
|
bridge: REST calls, client-observed WebSocket events, authentication changes, or
|
|
610
612
|
middleware/loader decisions.
|
|
611
613
|
|
|
612
|
-
Server Actions
|
|
613
|
-
|
|
614
|
-
the
|
|
615
|
-
|
|
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,
|
|
616
621
|
`invalidateClientCache()` and `keepClientCache()` give application code control in
|
|
617
622
|
both directions — force a miss for mutations the router cannot observe, or preserve
|
|
618
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
|
|
@@ -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/outlets.md
CHANGED
|
@@ -28,6 +28,10 @@ function DashboardLayout() {
|
|
|
28
28
|
`ParallelOutlet` requires the name and reads clearer at the call site. See
|
|
29
29
|
`/layout` and `/parallel` for the server side.
|
|
30
30
|
|
|
31
|
+
An outlet that moves between a layout's branches (bare in one, wrapped in
|
|
32
|
+
another) remounts its children; see `/layout`, "Moving the Outlet remounts its
|
|
33
|
+
children".
|
|
34
|
+
|
|
31
35
|
### useOutlet()
|
|
32
36
|
|
|
33
37
|
Access outlet content and its client-route presentation state programmatically:
|
package/skills/hooks/state.md
CHANGED
|
@@ -365,7 +365,15 @@ path("/checkout/callback", async (ctx) => {
|
|
|
365
365
|
- **`ppr` and cached routes.** A shell HIT and a `cache()` hit run no
|
|
366
366
|
handler, so a handler there cannot set state on that request. Set it from
|
|
367
367
|
middleware, which runs on every request; the HIT still serves, with the
|
|
368
|
-
state in its live payload.
|
|
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.
|
|
369
377
|
- **The cookie.** It is named after the router's state cookie
|
|
370
378
|
(`<stateCookieName>-ls`, `__Host-` prefixed on https), `HttpOnly`,
|
|
371
379
|
`SameSite=Lax`, `Path=/`, and lives 60 seconds. Only the next GET document
|
|
@@ -378,7 +386,11 @@ path("/checkout/callback", async (ctx) => {
|
|
|
378
386
|
is one such cookie per router: two document redirects within a moment (two
|
|
379
387
|
tabs) keep only the later one's state. Without https there is no `__Host-`
|
|
380
388
|
prefix, so a sibling subdomain could set the cookie: treat state that
|
|
381
|
-
arrives this way as a message to show, not as a fact to act on.
|
|
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.
|
|
382
394
|
- **Known limit: a redirect from one router to another.** The cookie is
|
|
383
395
|
named after the redirecting router's state cookie, which holds its router
|
|
384
396
|
id (`<stateCookiePrefix>_<routerId>`), and a router reads only its own. On
|
|
@@ -607,9 +619,12 @@ from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
|
607
619
|
it is selected by export conditions:
|
|
608
620
|
|
|
609
621
|
- **Client** (client component or module): marks the history cache stale,
|
|
610
|
-
flushes the prefetch cache, rotates the router state, and notifies other
|
|
611
|
-
|
|
612
|
-
|
|
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).
|
|
613
628
|
- **Server** (handler, server component, middleware, loader, or action): writes
|
|
614
629
|
a rotated state `Set-Cookie` for the responding client, which marks its
|
|
615
630
|
caches stale when it next reads them. Idempotent within a request; throws
|
|
@@ -618,9 +633,10 @@ it is selected by export conditions:
|
|
|
618
633
|
from handler code writes the cookie only on the renders that run it (a
|
|
619
634
|
MISS); call it from middleware, which runs on every request, HITs included.
|
|
620
635
|
|
|
621
|
-
Server actions already
|
|
622
|
-
action, call `keepClientCache()` inside it
|
|
623
|
-
|
|
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").
|
|
624
640
|
|
|
625
641
|
```tsx
|
|
626
642
|
"use client";
|
|
@@ -278,9 +278,29 @@ gives `from.url.pathname === "/list/open"`, not `/item/1`. That differs from
|
|
|
278
278
|
`transition({ when })`'s `from`, which is always the committed location on
|
|
279
279
|
screen. `routeName` is set for named routes only. Pass an array of predicates
|
|
280
280
|
for AND logic (all must return true). Omit `when` entirely and the intercept
|
|
281
|
-
always activates.
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
always activates. A selector that throws does not intercept: the full page
|
|
282
|
+
renders and the error is logged with the route name.
|
|
283
|
+
|
|
284
|
+
A server action from an open intercept re-renders that intercept, the one
|
|
285
|
+
whose slot the page holds, and opens no other intercept that targets the
|
|
286
|
+
route. Its `when` is not evaluated again, so the intercept stays open after
|
|
287
|
+
the action even when its `when` would now reject. The page can hold the slots
|
|
288
|
+
of more than one of the route's intercepts: two intercepts on one layout that
|
|
289
|
+
fill the same slot share it, and a `parallel()` of an intercept's slot name
|
|
290
|
+
holds that slot whether or not an intercept is open there. Whether they are
|
|
291
|
+
declared for the route's name or its local name inside `include()`, the
|
|
292
|
+
action sets aside the intercepts whose slot a `parallel()` fills on their
|
|
293
|
+
layout and renders the one that is left, its `when` not evaluated. When
|
|
294
|
+
several are left (the same-slot pair), the action evaluates their `when`
|
|
295
|
+
again, with `from` the intercept's source page, and the first that passes
|
|
296
|
+
renders; when none passes, the first of them renders. When a `parallel()`
|
|
297
|
+
fills every held slot, that rule applies to all of them.
|
|
298
|
+
|
|
299
|
+
`when` selects an intercept; it does not authorize one. An action request
|
|
300
|
+
that names an intercept's slot renders that intercept without its `when`,
|
|
301
|
+
and a navigation request can claim any source page. Guard data in the target
|
|
302
|
+
route's middleware, which runs for every intercept render, or in the
|
|
303
|
+
intercept's loaders.
|
|
284
304
|
|
|
285
305
|
The server learns `from` from the `X-RSC-Router-Client-Path` header, which the
|
|
286
306
|
rango client sends on every navigation. A partial request without it falls
|
|
@@ -291,12 +311,13 @@ intercepted: it gets the full page.
|
|
|
291
311
|
### `prefetch: false` on an intercept's loaders
|
|
292
312
|
|
|
293
313
|
`loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
|
|
294
|
-
of a prefetch of the target from the source page
|
|
314
|
+
of a viewport or render prefetch of the target from the source page (a hover
|
|
315
|
+
prefetch runs it, and its click opens the modal complete with no fill):
|
|
295
316
|
|
|
296
317
|
```typescript
|
|
297
318
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
298
319
|
loader(ProductLoader),
|
|
299
|
-
loader(ReviewsLoader, { prefetch: false }), // skipped in
|
|
320
|
+
loader(ReviewsLoader, { prefetch: false }), // skipped in a viewport prefetch
|
|
300
321
|
loading(<ModalSkeleton />),
|
|
301
322
|
]);
|
|
302
323
|
```
|
|
@@ -311,13 +332,13 @@ flagged loader with `ctx.use()` before its first `await` (or one with no
|
|
|
311
332
|
`loading()`) gets it run in the prefetch instead: one run, no fill for it.
|
|
312
333
|
`loading(…, { prefetch: false })` inside `intercept()` defers nothing.
|
|
313
334
|
|
|
314
|
-
A prefetch of a route an intercept targets
|
|
315
|
-
or not the intercept applies from that page
|
|
316
|
-
`x-rsc-prefetch-scope: source`, set from
|
|
317
|
-
full page prefetched from a page where
|
|
318
|
-
from a page where it is true.
|
|
319
|
-
|
|
320
|
-
segment (`MatchResult.sourceHeld`, same header).
|
|
335
|
+
A prefetch of a route an intercept targets, or one sent from such a route, is
|
|
336
|
+
stored per source page, whether or not the intercept applies from that page
|
|
337
|
+
(`rsc-rendering.ts` `x-rsc-prefetch-scope: source`, set from
|
|
338
|
+
`MatchResult.interceptTargeted`). A full page prefetched from a page where
|
|
339
|
+
`when` is false never serves a click from a page where it is true. Other
|
|
340
|
+
prefetches keep one shared entry, unless a `revalidate()` that read the source
|
|
341
|
+
page kept a segment (`MatchResult.sourceHeld`, same header).
|
|
321
342
|
|
|
322
343
|
```typescript
|
|
323
344
|
// Intercept only when opened from the shop index
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -12,6 +12,12 @@ layout stays mounted while you navigate between the routes it wraps. This skill
|
|
|
12
12
|
covers layout forms, outlets, orphan layouts (a layout inside a `path()`), and
|
|
13
13
|
layout revalidation.
|
|
14
14
|
|
|
15
|
+
**Rule: a layout keeps its `<Outlet />` in one place on every branch.** Branch
|
|
16
|
+
around the outlet, never around it and the outlet together: an early
|
|
17
|
+
`return <Outlet />` beside a wrapped one remounts every child segment each time
|
|
18
|
+
the branch flips (state resets, mount effects and the actions they call run
|
|
19
|
+
again). See [Moving the Outlet remounts its children](#moving-the-outlet-remounts-its-children).
|
|
20
|
+
|
|
15
21
|
Related skills: `/parallel` (named slots rendered by a layout), `/intercept`
|
|
16
22
|
(modal slots), `/route` (the routes a layout wraps), `/router-setup` (the
|
|
17
23
|
`document` shell that renders `<html>`).
|
|
@@ -131,6 +137,49 @@ function ShopLayout() {
|
|
|
131
137
|
|
|
132
138
|
A layout's `transition()` config wraps the content that flows through `<Outlet />` — not the layout chrome itself, and not sibling `<ParallelOutlet />` slots. Stacking transitions across nested layouts collapses around the deepest default outlet content. See [skills/view-transitions](../view-transitions/SKILL.md) for the full wrap rules and intercept-modal interaction.
|
|
133
139
|
|
|
140
|
+
### Moving the Outlet remounts its children
|
|
141
|
+
|
|
142
|
+
React keeps a subtree mounted only while each element on its path keeps the
|
|
143
|
+
same type at the same position. A layout that returns a bare `<Outlet />` on
|
|
144
|
+
one branch and wraps it on another (a fragment, a `<div>`) moves the outlet,
|
|
145
|
+
so when the branch flips, every child segment unmounts and mounts again:
|
|
146
|
+
client state resets, mount effects run again (an action they call is sent
|
|
147
|
+
again), and the content flashes. Prefer one shape with the branch inside it:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// Remounts the children whenever `promo` flips
|
|
151
|
+
function CategoryLayout({ promo }: { promo: boolean }) {
|
|
152
|
+
if (!promo) return <Outlet />;
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<PromoBanner />
|
|
156
|
+
<Outlet />
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Keeps them mounted: the outlet keeps its position
|
|
162
|
+
function CategoryLayout({ promo }: { promo: boolean }) {
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
{promo ? <PromoBanner /> : null}
|
|
166
|
+
<Outlet />
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A stable `key` on the outlet (`<Outlet key="outlet" />` in both branches)
|
|
173
|
+
also keeps the children mounted when the only difference is a top-level
|
|
174
|
+
fragment: React reads a component's unkeyed top-level fragment as a list of
|
|
175
|
+
siblings and matches the keyed outlet across it. It does not help when one
|
|
176
|
+
branch wraps the outlet in an element (a `<div>`, a provider): the outlet then
|
|
177
|
+
sits one level deeper, and keys match only among siblings.
|
|
178
|
+
|
|
179
|
+
All of this applies to `<ParallelOutlet />` too. To reset the children on
|
|
180
|
+
purpose, change a `key` on a stable wrapper rather than relying on a shape
|
|
181
|
+
change.
|
|
182
|
+
|
|
134
183
|
## Named Outlets
|
|
135
184
|
|
|
136
185
|
For parallel routes, use named outlets (`<Outlet name="@sidebar" />` is
|
|
@@ -267,6 +316,17 @@ layout(LocaleLayout, () => [
|
|
|
267
316
|
])
|
|
268
317
|
```
|
|
269
318
|
|
|
319
|
+
A rule that reads `currentParams` or `fromRouteName` and keeps the layout
|
|
320
|
+
shares a prefetch between the pages with equal values of what it read: the
|
|
321
|
+
locale rule above keeps the layout on every `/en` to `/en` navigation, so one
|
|
322
|
+
prefetch of a link serves every `/en` page, and a French page prefetches its
|
|
323
|
+
own. Spreading `currentParams` reads every param, so read the ones the rule
|
|
324
|
+
needs by name; a rule that reads `currentUrl` makes the prefetch page-only
|
|
325
|
+
(`/links` → "Shared prefetches and `prefetchKey`").
|
|
326
|
+
Write it for chrome that must follow the param; a block that belongs to one
|
|
327
|
+
page goes in its route or in an orphan layout inside its `path()` (above),
|
|
328
|
+
which re-renders with the route and needs no rule.
|
|
329
|
+
|
|
270
330
|
`revalidate(false)` on a standalone layout only restates the default.
|
|
271
331
|
The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
|
|
272
332
|
and the `revalidate(true | false)` shorthand are covered in `/loader` →
|