@rangojs/router 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/package.json
CHANGED
package/skills/caching/SKILL.md
CHANGED
|
@@ -453,6 +453,28 @@ or a non-cacheable variable fails on a miss unless the loader `cache()` has a
|
|
|
453
453
|
a `keyGenerator`. Either one switches the check off, so it must itself include
|
|
454
454
|
the value (`/loader` → "Cache Key").
|
|
455
455
|
|
|
456
|
+
### Loaders and `prefetch: false`
|
|
457
|
+
|
|
458
|
+
A loader registered with `{ prefetch: false }` is skipped in a `<Link>`
|
|
459
|
+
prefetch, and its own `cache()` is not read either, even when it would hit:
|
|
460
|
+
the fill request that the click sends afterwards reads the loader cache and
|
|
461
|
+
runs the loader on a miss. A route-level `cache()` boundary is unaffected: its
|
|
462
|
+
stored handler output is served and written as before, hit or miss, and
|
|
463
|
+
`loading(fallback, { prefetch: false })` under it defers only the loaders
|
|
464
|
+
behind the fallback, never the cached handler. A `cache()` route below a
|
|
465
|
+
layout with a flagged `loading()` keeps using its record whenever the browser
|
|
466
|
+
already holds that layout; only a prefetch that defers the layout itself (it
|
|
467
|
+
is new to the page) leaves the record unread and unwritten. A response that
|
|
468
|
+
carries deferred work is never reused: the document cache refuses it, and it
|
|
469
|
+
is sent `cache-control: private, no-cache` with `X-Rango-Prefetch` added to
|
|
470
|
+
`Vary`, whatever `Cache-Control` the route set, so no CDN keeps it and it
|
|
471
|
+
never answers a navigation. A browser may keep a copy and answer the same
|
|
472
|
+
prefetch from it while it reloads the page for back/forward; the click
|
|
473
|
+
completes it like any other. A fill is never answered from a stored body.
|
|
474
|
+
Every body the document cache does hold is complete, so one entry answers a
|
|
475
|
+
prefetch and a navigation, as without the flag. See `/loader` →
|
|
476
|
+
"`prefetch: false`".
|
|
477
|
+
|
|
456
478
|
## Global Cache Configuration
|
|
457
479
|
|
|
458
480
|
Configure the app-level store on the router. `cache()` boundaries, cached
|
|
@@ -621,11 +643,26 @@ const router = createRouter<AppBindings>({
|
|
|
621
643
|
|
|
622
644
|
**How the two layers work:**
|
|
623
645
|
|
|
624
|
-
| Scenario
|
|
625
|
-
|
|
|
626
|
-
| Hot request
|
|
627
|
-
| Cold colo
|
|
628
|
-
| First render
|
|
646
|
+
| Scenario | L1 (Cache API) | L2 (KV) | Result |
|
|
647
|
+
| ------------------------------------ | ---------------- | --------------------------- | ----------------------------- |
|
|
648
|
+
| Hot request | HIT | — | Serve from L1 (fast) |
|
|
649
|
+
| Cold colo | MISS | HIT | Serve from KV, promote to L1 |
|
|
650
|
+
| First render | MISS | MISS | Render, write to both L1 + KV |
|
|
651
|
+
| After `updateTag`, `kvFallThrough` | copy invalidated | HIT, written after the call | Serve from KV, promote to L1 |
|
|
652
|
+
| After `updateTag`, `kvFallThrough` | copy invalidated | MISS, or the old entry | Render, write to both L1 + KV |
|
|
653
|
+
| After `updateTag`, the default (off) | copy invalidated | not read | Render, write to both L1 + KV |
|
|
654
|
+
|
|
655
|
+
By default a copy a tag invalidation rejected is a miss and the colo renders.
|
|
656
|
+
`new CFCacheStore({ ctx, kv, kvFallThrough: true })` makes it read KV like a
|
|
657
|
+
cold colo, so the colo serves the entry another colo already rebuilt (a
|
|
658
|
+
visitor's render there, or a `router.prerender()` warm) instead of rebuilding
|
|
659
|
+
it. The KV entry passes the same tag check: one written before the invalidation
|
|
660
|
+
is never served. The cost is one KV get per rejected copy per colo after each
|
|
661
|
+
invalidation (a `ppr` document reads a shell, a record and each `"use cache"`
|
|
662
|
+
result, every copy rejected and the newer entry in KV: 150 ms added, measured
|
|
663
|
+
in #1069 at 50 ms per KV read locally, not on a deployed worker).
|
|
664
|
+
Turn it on for several locations where a render costs more than a KV read;
|
|
665
|
+
leave it off otherwise. It needs `kv`.
|
|
629
666
|
|
|
630
667
|
KV entries require `expirationTtl >= 60s`. Short-lived data entries (< 60s total
|
|
631
668
|
TTL) are only cached in L1. A PPR shell is always written to KV, a short one
|
|
@@ -844,10 +881,22 @@ another, request-scoped APIs are guarded inside a cache scope:
|
|
|
844
881
|
| Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
|
|
845
882
|
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
846
883
|
| `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
|
|
884
|
+
| `ctx.rewrittenFrom` (any ctx, `getRequestContext().rewrittenFrom`) | **throws** — the visitor's URL of a rewritten request |
|
|
847
885
|
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
848
886
|
| Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
|
|
849
887
|
| Any read in the boundary's own `key()`, `keyGenerator`, `condition()` or `tags()` | **allowed** — it picks or labels the entry, never rendered |
|
|
850
888
|
|
|
889
|
+
`ctx.rewrittenFrom` (the visitor's URL when middleware rewrote the request,
|
|
890
|
+
`/middleware` → "Rewrite") throws on every read inside a `cache()` boundary
|
|
891
|
+
or a `"use cache"` function, on a request nothing rewrote too, like
|
|
892
|
+
`cookies()`: the entry would hold whichever URL the first caller came from,
|
|
893
|
+
and a read that worked only on direct visits would break the first time a
|
|
894
|
+
rewritten request reached it. A PPR shell capture and a render the document
|
|
895
|
+
cache stores read it as `undefined` instead of throwing: those renders are
|
|
896
|
+
the target page as a direct visit renders it, and the rewritten visitor's
|
|
897
|
+
own render (outside them) still sees its URL. A response route under
|
|
898
|
+
`cache()` whose handler read it is not stored.
|
|
899
|
+
|
|
851
900
|
A loader body invoked from a handler with `await ctx.use(Loader)` may read
|
|
852
901
|
`cookies()`/`headers()`/`ctx.request.headers`, but its response writes still throw: on a hit the
|
|
853
902
|
handler is skipped, so that loader never runs.
|
package/skills/catalog.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": "client-urls",
|
|
38
|
-
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites,
|
|
38
|
+
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, server-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
|
|
39
39
|
"argumentHint": "[setup]",
|
|
40
40
|
"path": "skills/client-urls/SKILL.md"
|
|
41
41
|
},
|
|
@@ -137,13 +137,13 @@
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "loader",
|
|
140
|
-
"description": "Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles,
|
|
140
|
+
"description": "Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, must be settled in the SSR'd document (ssr:false), or must stay out of link prefetches (prefetch:false).",
|
|
141
141
|
"argumentHint": "[loader]",
|
|
142
142
|
"path": "skills/loader/SKILL.md"
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "middleware",
|
|
146
|
-
"description": "Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.",
|
|
146
|
+
"description": "Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects, rewrites (throw rewrite() renders another route at the same URL). Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, showing a maintenance page at the visitor's URL, or deciding whether middleware wraps server actions.",
|
|
147
147
|
"argumentHint": "[middleware-name]",
|
|
148
148
|
"path": "skills/middleware/SKILL.md"
|
|
149
149
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: client-urls
|
|
3
|
-
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites,
|
|
3
|
+
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, server-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
|
|
4
4
|
argument-hint: "[setup]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,7 +10,8 @@ argument-hint: "[setup]"
|
|
|
10
10
|
components are client components, and there are **no handlers at all** — every
|
|
11
11
|
data read is `useLoader` at the consumption site, waiting is scoped by the
|
|
12
12
|
inline `<Suspense>` above each read, and per-loader `revalidate()` predicates
|
|
13
|
-
run
|
|
13
|
+
run on the server (the build moves them out of the `"use client"` module).
|
|
14
|
+
The server shop pattern — `await ctx.use(ProductLoader)` in
|
|
14
15
|
a handler — is pipeline-blocking by contract: no pending UI can ever cover that
|
|
15
16
|
latency. `clientUrls()` makes that shape unrepresentable.
|
|
16
17
|
|
|
@@ -20,9 +21,9 @@ layout, mostly tab/param/filter switches. Three things compound: the
|
|
|
20
21
|
definition also matches in the browser, so a soft navigation renders the
|
|
21
22
|
destination component immediately (loader reads suspend into `loading()` or
|
|
22
23
|
an inline `<Suspense>`; `useOutlet().pending` flips for chrome) with no server
|
|
23
|
-
round-trip to start;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
round-trip to start; per-loader `revalidate()` predicates, run on the server
|
|
25
|
+
with the same arguments as in `urls()`, HOLD data across navigations that
|
|
26
|
+
don't invalidate it (a tab switch re-runs nothing); and any read that does refresh streams behind its
|
|
26
27
|
own inline boundary instead of gating the commit. Add per-Link prefetch
|
|
27
28
|
(`/links`) and a warmed click lands the complete page instantly.
|
|
28
29
|
|
|
@@ -131,9 +132,9 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
|
|
|
131
132
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
132
133
|
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see `/ppr` → The loader lane rule); no response variants. Use callback may contain `loader()`, `loading()`, `transition()` |
|
|
133
134
|
| `layout()` | `layout(Component, () => [...])` — children are required and must contain at least one `path()`; may also hold `loader()`, `loading()`, `intercept()`, nested `layout()` |
|
|
134
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below
|
|
135
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false }, use?)` — see below |
|
|
135
136
|
| `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
|
|
136
|
-
| `revalidate()` | Valid **inside a loader() use callback only**; runs
|
|
137
|
+
| `revalidate()` | Valid **inside a loader() use callback only**; runs on the server (the build hoists it out of the `"use client"` module) |
|
|
137
138
|
| `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
|
|
138
139
|
| `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
|
|
139
140
|
|
|
@@ -170,7 +171,8 @@ DIFFERENT schedules:
|
|
|
170
171
|
- **Middleware runs on EVERY canonical request**, not just the first
|
|
171
172
|
encounter. Every navigation inside the group — including a tab switch whose
|
|
172
173
|
loaders all hold — still sends the canonical partial request (it commits
|
|
173
|
-
URL/history and
|
|
174
|
+
URL/history, and the server runs each loader's `revalidate()` while it
|
|
175
|
+
handles it), and middleware wraps
|
|
174
176
|
each one: `requireAdmin` re-authorizes every navigation, and its
|
|
175
177
|
`ctx.set()` variables are fresh for the group's loaders on every pass. The
|
|
176
178
|
instant feel comes from held data and optimistic presentation, not from
|
|
@@ -187,22 +189,62 @@ All three pinned by dev + production e2e in the router repository (a
|
|
|
187
189
|
middleware-stamped header per request, a layout run-count stamp held across
|
|
188
190
|
navigations).
|
|
189
191
|
|
|
190
|
-
## Revalidation runs
|
|
191
|
-
|
|
192
|
-
A
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`
|
|
192
|
+
## Revalidation runs on the server
|
|
193
|
+
|
|
194
|
+
A `clientUrls()` loader's `revalidate()` runs only on the server, with exactly
|
|
195
|
+
the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
|
|
196
|
+
`@rangojs/router`): `currentParams`, `currentUrl`, `nextParams`, `nextUrl`,
|
|
197
|
+
`defaultShouldRevalidate`, `context` (the handler context, so
|
|
198
|
+
`context.get(var)` works), `segmentType`, `actionId`, `isAction(...refs)`,
|
|
199
|
+
`actionUrl`, `actionResult`, `formData`, `method`,
|
|
200
|
+
`routeName`/`fromRouteName`/`toRouteName`, and `stale`. Nothing runs in the
|
|
201
|
+
browser before the request goes out; there is no client-computed decision and
|
|
202
|
+
no hybrid.
|
|
203
|
+
|
|
204
|
+
You still write the predicate in the `"use client"` module. The rango Vite
|
|
205
|
+
plugin hoists each `revalidate()` argument into a server-only module, leaves a
|
|
206
|
+
key in the browser and SSR copies, and registers the predicate when the
|
|
207
|
+
group's module loads on the server (a lazy `include()` stays lazy). The cost
|
|
208
|
+
is a rule: **the argument may reference imports and globals only.** Anything
|
|
209
|
+
else is a build error naming `file:line:column`, starting
|
|
210
|
+
`[rango] clientUrls() revalidate() at ...: revalidate() runs on the server; ...`.
|
|
211
|
+
|
|
212
|
+
- Accepted: an inline arrow or function expression, an imported predicate
|
|
213
|
+
(`revalidate(isCartAction)`), a namespace member (`P.cart`), a call of an
|
|
214
|
+
imported factory (`makePredicate("cart")`), and globals such as `URL`,
|
|
215
|
+
`Boolean` and `import.meta.env`.
|
|
216
|
+
- A binding declared in the same `"use client"` module is rejected: move the
|
|
217
|
+
predicate to its own module without `"use client"` and import it, or write it
|
|
218
|
+
inline. A binding of an enclosing function is rejected: read it from the
|
|
219
|
+
predicate's argument. So are `this`, `arguments`, `super`, `new.target`,
|
|
220
|
+
`import()` and `import.meta` other than `import.meta.env`.
|
|
221
|
+
- A browser-only global (`window`, `document`, `navigator`, `localStorage`,
|
|
222
|
+
`sessionStorage`, `location`, `history`) is rejected: put the input on the
|
|
223
|
+
request (a cookie or a search param) and read it from the argument, or
|
|
224
|
+
refresh with `useRefreshLoaders`.
|
|
225
|
+
- An `async` or generator function, a spread, a second argument, no argument,
|
|
226
|
+
or a non-function value (object, array or string literal) is rejected.
|
|
227
|
+
- An import of `client-only`, or of a module whose directive prologue is
|
|
228
|
+
`"use client"`, is rejected (on the server its exports are client
|
|
229
|
+
references). A `"use server"` import is fine: on the server it is the real
|
|
230
|
+
action, which `isAction(ref)` matches. `server-only` and `node:` imports work
|
|
231
|
+
on the server, but the import also stays in the `"use client"` module
|
|
232
|
+
(imports are not pruned from the browser bundle), so it must still load in
|
|
233
|
+
the browser.
|
|
234
|
+
|
|
235
|
+
Per request: a JS action POST runs the predicate with `method: "POST"`,
|
|
236
|
+
`actionId`, `actionResult` (the action's return value), `formData` (the last
|
|
237
|
+
`FormData` argument) and `actionUrl`; the default is true. An in-group
|
|
238
|
+
navigation GET defaults to true when params or search changed. A prefetch
|
|
239
|
+
partial runs the predicate, so a prefetched in-group navigation honours it. A
|
|
240
|
+
stale back/forward revalidation sees `stale: true`. Document requests and
|
|
241
|
+
no-JS form posts hold no segments, so every loader renders whatever a predicate
|
|
242
|
+
says. On the action-triggered refetch GET (consolidation) `isAction()` is false,
|
|
243
|
+
as for `urls()`. `currentParams`/`nextParams` are the server's route params,
|
|
244
|
+
including the `include()` mount's params, and `actionId` is the server's
|
|
245
|
+
`src/...#export` form; match actions with `isAction(ref)` rather than a
|
|
246
|
+
substring. Vitest does not run the plugin, so in unit tests the predicate stays
|
|
247
|
+
a plain function: test it with `runRevalidate` (`/testing`).
|
|
206
248
|
|
|
207
249
|
```ts
|
|
208
250
|
import { addToCart, removeFromCart } from "./actions/cart";
|
|
@@ -211,6 +253,11 @@ import * as CartActions from "./actions/cart";
|
|
|
211
253
|
// After an action, re-run only for cart actions; on navigation, keep the default.
|
|
212
254
|
revalidate(({ isAction }) => (isAction() ? isAction(CartActions) : undefined));
|
|
213
255
|
|
|
256
|
+
// Decide on what the action returned (actionResult is visible on the server):
|
|
257
|
+
revalidate((ctx) =>
|
|
258
|
+
ctx.isAction(checkout) ? ctx.actionResult?.ok === true : undefined,
|
|
259
|
+
);
|
|
260
|
+
|
|
214
261
|
// Matcher forms (each returns a boolean):
|
|
215
262
|
isAction(); // any action
|
|
216
263
|
isAction(addToCart); // one action
|
|
@@ -226,6 +273,11 @@ with the ternary above instead (same rule as a server-tree loader). A bare
|
|
|
226
273
|
`isAction(refs)` is a hard `false` on navigation too, so the loader stops
|
|
227
274
|
refetching when params change.
|
|
228
275
|
|
|
276
|
+
`intercept()` loaders inside `clientUrls()` do not apply their `revalidate()`
|
|
277
|
+
yet (a known gap, left for a follow-up). The build still checks those
|
|
278
|
+
predicates like any other, so one that captures a binding of the `"use client"`
|
|
279
|
+
module fails the build although nothing runs it: import it, or drop it.
|
|
280
|
+
|
|
229
281
|
Two scars worth copying:
|
|
230
282
|
|
|
231
283
|
- A blunt `() => false` keeps serving the OLD product on product→product
|
|
@@ -235,6 +287,80 @@ Two scars worth copying:
|
|
|
235
287
|
actions (`isAction(CartActions)`) while product/related loaders hold —
|
|
236
288
|
three freshness outcomes in a single commit, decided per loader.
|
|
237
289
|
|
|
290
|
+
## After an action: what the server decides
|
|
291
|
+
|
|
292
|
+
The browser sends the action with the ids of the segments the page holds.
|
|
293
|
+
Everything after that is decided on the server, while it handles that request:
|
|
294
|
+
|
|
295
|
+
- **The action** runs there, and its return value is what the call resolves
|
|
296
|
+
with (`await action()`, `useActionState`).
|
|
297
|
+
- **Which loaders rerun.** Each group loader's own `revalidate()` answers, with
|
|
298
|
+
the action's `actionResult` and `formData` (above). A loader that holds keeps
|
|
299
|
+
the data the page shows.
|
|
300
|
+
- **The browser's caches.** When the response arrives the browser marks its
|
|
301
|
+
history cache stale and drops its prefetches, unless the action called
|
|
302
|
+
`keepClientCache()` (`/server-actions` → "Client Cache After an Action").
|
|
303
|
+
- **A loader that fails on its rerun** reaches your `onError` on the server and
|
|
304
|
+
the page as an error, below.
|
|
305
|
+
|
|
306
|
+
A form's React state survives an action's revalidation in a group, as on a
|
|
307
|
+
`urls()` route: the commit renders into the mounted page instead of remounting
|
|
308
|
+
it, so a controlled input keeps what the user typed.
|
|
309
|
+
|
|
310
|
+
### A loader that fails on its rerun
|
|
311
|
+
|
|
312
|
+
The action has already run when its loaders rerun, so a loader that throws
|
|
313
|
+
there does not undo it. Where the error lands decides what the user sees:
|
|
314
|
+
|
|
315
|
+
| The route | Called directly (`await action()`) | From `<form action>`, `useActionState` or `useTransition` |
|
|
316
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
317
|
+
| A group route, or a `urls()` route without `loading()` | The call rejects with the loader's error (`"An error occurred"` in a build), although the action returned. Nothing of that revalidation commits: the page keeps its data and its React state. | React hands the rejection to the nearest React error boundary above the form. With none of yours, the router's root error boundary replaces the page. |
|
|
318
|
+
| A `urls()` route with `loading()` | The call resolves with the action's result. The commit renders, the loader's read throws, and the nearest React error boundary catches it; with none of yours, the root one replaces the page. | The same. |
|
|
319
|
+
| Under an `errorBoundary()` (for a group, around the mount) | Its fallback replaces the route: the page and its form state unmount. | The same. |
|
|
320
|
+
|
|
321
|
+
A group route's `loading()` does not change the first row. For a failure the
|
|
322
|
+
user can retry (a flaky upstream, a rate limit), return it as data instead of
|
|
323
|
+
throwing, and render it where the data goes:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
// stock.loader.ts
|
|
327
|
+
export const StockLoader = createLoader(async (ctx) => {
|
|
328
|
+
try {
|
|
329
|
+
return { ok: true as const, stock: await inventory.count(ctx.params.sku) };
|
|
330
|
+
} catch {
|
|
331
|
+
return { ok: false as const, reason: "Stock is unavailable. Try again." };
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// in the group's "use client" module
|
|
336
|
+
function Stock() {
|
|
337
|
+
const { data } = useLoader(StockLoader);
|
|
338
|
+
return data.ok ? (
|
|
339
|
+
<p>{data.stock} in stock</p>
|
|
340
|
+
) : (
|
|
341
|
+
<p role="status">{data.reason}</p>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The action resolves with its result, the page and its form state stay, and the
|
|
347
|
+
next action or navigation that reruns the loader replaces the message. Keep
|
|
348
|
+
throwing for a failure that should stop the page, with an `errorBoundary()`
|
|
349
|
+
around the mount for it.
|
|
350
|
+
|
|
351
|
+
A failure returned as data is an ordinary value, so whatever stores the
|
|
352
|
+
loader's value stores it too: a `cache()` on the loader serves it for its TTL,
|
|
353
|
+
a `ppr` shell bakes an `ssr: false` loader's value, and a prerender stores it
|
|
354
|
+
at build time. A thrown error is never stored (a stale entry keeps serving).
|
|
355
|
+
To cache the expensive part, cache the call the loader makes (`"use cache"` on
|
|
356
|
+
that function: a call that throws stores nothing) and turn the throw into
|
|
357
|
+
data in a loader that is not cached itself.
|
|
358
|
+
|
|
359
|
+
Pinned by dev + production e2e in the router repository (a group, its `urls()`
|
|
360
|
+
twin, each with and without `loading()`, and the group under an
|
|
361
|
+
`errorBoundary()`), and by a `serveShellRequest({ action })` test of the
|
|
362
|
+
response.
|
|
363
|
+
|
|
238
364
|
## Loaders are full citizens: signals and handles
|
|
239
365
|
|
|
240
366
|
Everything a DSL loader can do in the server tree works here — the loader body
|
|
@@ -321,6 +447,53 @@ need it, not the route.
|
|
|
321
447
|
|
|
322
448
|
Pinned end to end by dev + production e2e in the router repository.
|
|
323
449
|
|
|
450
|
+
**Without JavaScript.** A boundary that is not complete at the first flush
|
|
451
|
+
ships its fallback and swaps the content in with a script, which a browser
|
|
452
|
+
without JavaScript never runs. That includes a client component a loader's
|
|
453
|
+
answer draws under an inline `<Suspense>`: a fast loader's answer usually
|
|
454
|
+
makes the first flush, and `ssr: false` guarantees the loader's data does. It
|
|
455
|
+
does not cover the client component's module: the first document a build's
|
|
456
|
+
fresh server renders can still be waiting for it. The dev server matches a
|
|
457
|
+
build here: when the shell is ready, it waits for the client components the
|
|
458
|
+
payload has named and that are still loading, and for nothing else.
|
|
459
|
+
|
|
460
|
+
## `prefetch: false` — keep a loader out of prefetches
|
|
461
|
+
|
|
462
|
+
A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
|
|
463
|
+
and the prefetch skips it:
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
467
|
+
loader(PriceLoader),
|
|
468
|
+
loader(ReviewsLoader, { prefetch: false }), // kept out of prefetches
|
|
469
|
+
loading(<ProductSkeleton />),
|
|
470
|
+
]),
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
A click inside the group still shows the destination at once. With the
|
|
474
|
+
prefetch received, the URL and the other loaders' data move with the click
|
|
475
|
+
too, `useOutlet().pending` ends with that commit, and one follow-up request
|
|
476
|
+
(the fill) runs only the flagged loaders. Read the flagged loader inside its
|
|
477
|
+
own `<Suspense>` to show the page and that fallback at once; with no boundary
|
|
478
|
+
React holds the page being left until the fill answers, as it would with no
|
|
479
|
+
prefetch. Same rules as on a server route (`/loader` → "`prefetch: false`"):
|
|
480
|
+
only a segment new to the page is deferred, so a same-route click
|
|
481
|
+
(`/product/a` to `/product/b`) prefetches the loader as usual.
|
|
482
|
+
|
|
483
|
+
- On a `layout()` of the group the flag applies to every route below it.
|
|
484
|
+
- A flagged `loader()` on a server layout above the group is deferred while
|
|
485
|
+
that layout is new to the page (entering the group from outside).
|
|
486
|
+
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
487
|
+
on a server layout above the group defers nothing for the group's routes.
|
|
488
|
+
- `intercept()` loaders accept the flag and ignore it, as in `urls()`.
|
|
489
|
+
- With `ssr: false` on a `ppr` group route the flag is ignored (the bake
|
|
490
|
+
lane), as on a server route.
|
|
491
|
+
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
492
|
+
itself, and `rangoUseClientTransform()` installs the group's projection
|
|
493
|
+
(`/testing` → `serve-shell-request.md`, "`clientUrls()` routes"). It also
|
|
494
|
+
registers the group's `revalidate()` predicates, so the server runs them
|
|
495
|
+
there as in your app.
|
|
496
|
+
|
|
324
497
|
## Intercepts
|
|
325
498
|
|
|
326
499
|
```tsx
|
|
@@ -292,20 +292,25 @@ traffic"). On Workers:
|
|
|
292
292
|
the request it is called from.
|
|
293
293
|
|
|
294
294
|
- **A warm fills the colo it runs in, and KV.** Another colo that already
|
|
295
|
-
holds its own Cache API copy keeps serving it until it expires
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
warm, in this order:
|
|
295
|
+
holds its own Cache API copy keeps serving it until it expires or a tag
|
|
296
|
+
invalidation rejects it; a hit on a valid copy never consults KV. After a
|
|
297
|
+
deploy no colo holds a copy, so the warm's KV entry is what each one reads
|
|
298
|
+
next. For a content change, invalidate and then warm, in this order:
|
|
299
299
|
|
|
300
300
|
```typescript
|
|
301
|
-
await updateTag(`product:${id}`); //
|
|
301
|
+
await updateTag(`product:${id}`); // a colo the marker has reached refuses its old copy
|
|
302
302
|
await router.prerender({ env, ctx })(`/products/${id}`); // writes the new one
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
-
A colo with no copy of its own reads the warmed entry from KV
|
|
306
|
-
was still holding the old copy
|
|
307
|
-
itself
|
|
308
|
-
|
|
305
|
+
A colo with no copy of its own then reads the warmed entry from KV and
|
|
306
|
+
renders nothing. A colo that was still holding the old copy renders once for
|
|
307
|
+
itself by default; with `kvFallThrough: true` on the store it reads KV as if
|
|
308
|
+
it held none and renders nothing (with `tagPurge` the purge evicts the copy
|
|
309
|
+
first). That option costs one KV get per rejected copy per colo after each
|
|
310
|
+
invalidation, so turn it on where a render costs more than that. The KV
|
|
311
|
+
entry is checked against the same tag markers as
|
|
312
|
+
the colo's own copy, so a colo that KV still hands the entry from before
|
|
313
|
+
the invalidation refuses it and renders for itself.
|
|
309
314
|
|
|
310
315
|
- **Entries under 60 seconds stay in one colo.** KV's minimum `expirationTtl`
|
|
311
316
|
is 60 s, so a segment, item or response entry whose `ttl + swr` is shorter
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -111,11 +111,24 @@ cannot mismatch its server HTML.
|
|
|
111
111
|
|
|
112
112
|
- Navigation state, params and action state: the component shows the live
|
|
113
113
|
value right after it hydrates.
|
|
114
|
-
- Handle data a loader pushed late, and the entry's location state:
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
- Handle data a loader pushed late, and the entry's location state: both wait
|
|
115
|
+
until the whole document has streamed and been revealed, and every boundary
|
|
116
|
+
has hydrated, including any boundary still suspended on the client (a lazy
|
|
117
|
+
chunk, a deferred-hydration gate; that wait has no upper bound). Then
|
|
118
|
+
location state lands in a transition and handle data in the release
|
|
119
|
+
notification. `useLocationState` readers re-render when the entry's
|
|
120
|
+
state object changes; `useHandle` readers when their selection changes.
|
|
121
|
+
- State set before then by a commit that renders no new page (a shallow
|
|
122
|
+
navigation with `revalidate: false`, Back/Forward or a revalidation that
|
|
123
|
+
keeps every segment, an action's location state) waits the same way: readers
|
|
124
|
+
keep `undefined` until every boundary has hydrated, then show the latest
|
|
125
|
+
entry's state.
|
|
117
126
|
- A navigation that commits before then mounts its page with the live values
|
|
118
127
|
from its first render. A boundary of the document that persists across that
|
|
119
128
|
navigation and has not hydrated yet then hydrates against the live values;
|
|
120
129
|
where they differ from its HTML, React reports the mismatch and renders that
|
|
121
130
|
boundary on the client.
|
|
131
|
+
- A held navigation whose history entry commits during that window releases
|
|
132
|
+
the store for the whole hold. A boundary of the old page that hydrates during
|
|
133
|
+
the hold reads the destination's values and can mismatch its HTML; React
|
|
134
|
+
then renders that boundary on the client.
|
package/skills/hooks/data.md
CHANGED
|
@@ -146,7 +146,9 @@ clears the shared error.
|
|
|
146
146
|
**`load()` calls that stay local** (no broadcast, per-hook state, same
|
|
147
147
|
semantics as the old per-component `useState`):
|
|
148
148
|
|
|
149
|
-
- `load({ params: { ... } })` — explicit params.
|
|
149
|
+
- `load({ params: { ... } })` — explicit params. They are string values; a
|
|
150
|
+
`__proto__`, `constructor` or `prototype` key or a non-string value is
|
|
151
|
+
rejected and `load()` fails with `Invalid loader params`.
|
|
150
152
|
- `load({ method: "POST", body })` — mutations.
|
|
151
153
|
- Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
|
|
152
154
|
registered on the current route. Two unrelated components calling
|
package/skills/links/SKILL.md
CHANGED
|
@@ -479,6 +479,30 @@ mounted viewport/render Links. Links share one document-level scope observer,
|
|
|
479
479
|
not one observer per Link or container. Adding a scope cannot recall work already
|
|
480
480
|
queued or in flight; removing it re-arms both Link types.
|
|
481
481
|
|
|
482
|
+
### What a prefetch renders
|
|
483
|
+
|
|
484
|
+
A prefetch is a navigation sent early (a partial request with an
|
|
485
|
+
`X-Rango-Prefetch` header), and by default the server renders everything for
|
|
486
|
+
it: every handler and every loader. That is the point, a click then commits
|
|
487
|
+
without a request, but it is also forty renders for a grid of forty links.
|
|
488
|
+
Keep the expensive part out of the prefetch on the route, not on the link:
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
// skip one loader
|
|
492
|
+
loader(ReviewsLoader, { prefetch: false }),
|
|
493
|
+
// skip what the fallback covers
|
|
494
|
+
loading(<OrdersSkeleton />, { prefetch: false }),
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
A click that adopts such a prefetch commits what was prefetched, shows the
|
|
498
|
+
fallback for the deferred part, and sends one extra request for it. Only what
|
|
499
|
+
is new to the page is deferred: a segment already on screen (the layout of the
|
|
500
|
+
section you are in, the route itself on `/product/a` to `/product/b`) is
|
|
501
|
+
prefetched as usual. See
|
|
502
|
+
`/loader` → "`prefetch: false`" and `/route` → "`loading()` options". To stop
|
|
503
|
+
a link or a whole section from prefetching at all, use the `prefetch` prop or
|
|
504
|
+
`data-prefetch-scope` above.
|
|
505
|
+
|
|
482
506
|
## When to use what
|
|
483
507
|
|
|
484
508
|
| Context | API | Resolves | Use for |
|