@rangojs/router 0.22.1 → 0.23.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 +8 -3
- 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/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- 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 +19 -9
- 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 +16 -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/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/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- 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 +14 -1
- 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 +3 -3
- 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/run-loader.d.ts +2 -1
- 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 +38 -0
- package/dist/types/testing/vitest.d.ts +11 -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/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 +11 -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 +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- 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 +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- 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 +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- 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 +37 -10
- 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.ts +22 -1
- 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/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- 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/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -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/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- 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/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -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/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- 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/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 +20 -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/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 +93 -32
- 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
|
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,7 +137,7 @@
|
|
|
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
|
},
|
|
@@ -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
|
|
@@ -321,6 +373,43 @@ need it, not the route.
|
|
|
321
373
|
|
|
322
374
|
Pinned end to end by dev + production e2e in the router repository.
|
|
323
375
|
|
|
376
|
+
## `prefetch: false` — keep a loader out of prefetches
|
|
377
|
+
|
|
378
|
+
A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
|
|
379
|
+
and the prefetch skips it:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
383
|
+
loader(PriceLoader),
|
|
384
|
+
loader(ReviewsLoader, { prefetch: false }), // kept out of prefetches
|
|
385
|
+
loading(<ProductSkeleton />),
|
|
386
|
+
]),
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
A click inside the group still shows the destination at once. With the
|
|
390
|
+
prefetch received, the URL and the other loaders' data move with the click
|
|
391
|
+
too, `useOutlet().pending` ends with that commit, and one follow-up request
|
|
392
|
+
(the fill) runs only the flagged loaders. Read the flagged loader inside its
|
|
393
|
+
own `<Suspense>` to show the page and that fallback at once; with no boundary
|
|
394
|
+
React holds the page being left until the fill answers, as it would with no
|
|
395
|
+
prefetch. Same rules as on a server route (`/loader` → "`prefetch: false`"):
|
|
396
|
+
only a segment new to the page is deferred, so a same-route click
|
|
397
|
+
(`/product/a` to `/product/b`) prefetches the loader as usual.
|
|
398
|
+
|
|
399
|
+
- On a `layout()` of the group the flag applies to every route below it.
|
|
400
|
+
- A flagged `loader()` on a server layout above the group is deferred while
|
|
401
|
+
that layout is new to the page (entering the group from outside).
|
|
402
|
+
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
403
|
+
on a server layout above the group defers nothing for the group's routes.
|
|
404
|
+
- `intercept()` loaders accept the flag and ignore it, as in `urls()`.
|
|
405
|
+
- With `ssr: false` on a `ppr` group route the flag is ignored (the bake
|
|
406
|
+
lane), as on a server route.
|
|
407
|
+
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
408
|
+
itself, and `rangoUseClientTransform()` installs the group's projection
|
|
409
|
+
(`/testing` → `serve-shell-request.md`, "`clientUrls()` routes"). It also
|
|
410
|
+
registers the group's `revalidate()` predicates, so the server runs them
|
|
411
|
+
there as in your app.
|
|
412
|
+
|
|
324
413
|
## Intercepts
|
|
325
414
|
|
|
326
415
|
```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,19 @@ 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. Location state
|
|
116
|
+
lands in the barrier commit (one transition that also hydrates every
|
|
117
|
+
boundary still dehydrated); handle data lands in the release notification
|
|
118
|
+
right after that commit. `useLocationState` readers re-render when the
|
|
119
|
+
entry's state object changes; `useHandle` readers when their selection
|
|
120
|
+
changes.
|
|
117
121
|
- A navigation that commits before then mounts its page with the live values
|
|
118
122
|
from its first render. A boundary of the document that persists across that
|
|
119
123
|
navigation and has not hydrated yet then hydrates against the live values;
|
|
120
124
|
where they differ from its HTML, React reports the mismatch and renders that
|
|
121
125
|
boundary on the client.
|
|
126
|
+
- A held navigation whose history entry commits during that window releases
|
|
127
|
+
the store for the whole hold. A boundary of the old page that hydrates during
|
|
128
|
+
the hold reads the destination's values and can mismatch its HTML; React
|
|
129
|
+
then renders that boundary on the client.
|
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 |
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
|
-
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,
|
|
3
|
+
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).
|
|
4
4
|
argument-hint: "[loader]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -936,6 +936,8 @@ function ProductPage() {
|
|
|
936
936
|
}
|
|
937
937
|
```
|
|
938
938
|
|
|
939
|
+
A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
|
|
940
|
+
|
|
939
941
|
## Loader Authority: notFound() and redirect()
|
|
940
942
|
|
|
941
943
|
A loader may **throw** `notFound()` and `redirect()` — data-dependent
|
|
@@ -1094,6 +1096,195 @@ The costs and constraints:
|
|
|
1094
1096
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
1095
1097
|
loader-heavy shape makes it most useful.
|
|
1096
1098
|
|
|
1099
|
+
## `prefetch: false` — Keep a Loader Out of Prefetches
|
|
1100
|
+
|
|
1101
|
+
A `<Link>` prefetch is a navigation sent early: the server renders every
|
|
1102
|
+
handler and every loader for it. With the default `"viewport"` strategy that
|
|
1103
|
+
means a grid of forty product links runs forty copies of your slowest loader
|
|
1104
|
+
for pages most visitors never open. Pass `prefetch: false` to keep one loader
|
|
1105
|
+
out:
|
|
1106
|
+
|
|
1107
|
+
```tsx
|
|
1108
|
+
import { Suspense } from "react";
|
|
1109
|
+
|
|
1110
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1111
|
+
loader(PriceLoader), // cheap: still runs in the prefetch
|
|
1112
|
+
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in a prefetch
|
|
1113
|
+
loading(<ProductSkeleton />),
|
|
1114
|
+
]),
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1118
|
+
`{ ssr: false, prefetch: false }` if you need both.)
|
|
1119
|
+
|
|
1120
|
+
What happens:
|
|
1121
|
+
|
|
1122
|
+
- A **prefetch** of the route does not run the loader and does not read its
|
|
1123
|
+
`cache()`; the payload marks it deferred. The rest of the page prefetches as
|
|
1124
|
+
usual.
|
|
1125
|
+
- Only on a route the browser does not show yet. A loader the page already
|
|
1126
|
+
holds, on a same-route navigation such as `/product/a` to `/product/b`, runs
|
|
1127
|
+
in the prefetch when it revalidates, exactly as without the flag, and the
|
|
1128
|
+
click sends no second request. The rule: a click that adopts a prefetch is
|
|
1129
|
+
never worse than the same click with no prefetch at all (one exception: a
|
|
1130
|
+
click before its prefetch has answered, under "The limits"), and content on
|
|
1131
|
+
screen is never swapped for a fallback because of the flag. For the same
|
|
1132
|
+
reason a prefetch that deferred something is used only on the page that
|
|
1133
|
+
made it: a page that already shows the segment makes its own prefetch.
|
|
1134
|
+
- A **click** that adopts that prefetch is never blocked by the loader. A
|
|
1135
|
+
deferred loader behaves like a loader that is still streaming, and where
|
|
1136
|
+
the page reads it (`useLoader(ReviewsLoader)`) React does what it does for
|
|
1137
|
+
one:
|
|
1138
|
+
- the read has its own `<Suspense>`: the page shows at once with that
|
|
1139
|
+
fallback in it. This can be a finer fallback than the same click shows
|
|
1140
|
+
with no prefetch, where the whole route is still streaming behind its
|
|
1141
|
+
`loading()`;
|
|
1142
|
+
- the nearest boundary is the route's `loading()`: that fallback shows;
|
|
1143
|
+
- no boundary at all: React holds the page being left until the value
|
|
1144
|
+
arrives.
|
|
1145
|
+
|
|
1146
|
+
Meanwhile the browser sends one second request, the fill, that runs only
|
|
1147
|
+
the deferred loader. Its value arrives in the page that is already on
|
|
1148
|
+
screen, as a streamed loader's does; nothing remounts.
|
|
1149
|
+
|
|
1150
|
+
- A **document request** (hard load), a **navigation with no prefetch to
|
|
1151
|
+
adopt**, an action, back/forward and a no-JS form post run the loader as
|
|
1152
|
+
they always did. Leaving the option out is the old behavior.
|
|
1153
|
+
|
|
1154
|
+
Read the loader behind a boundary, as you would any streamed loader:
|
|
1155
|
+
|
|
1156
|
+
```tsx
|
|
1157
|
+
"use client";
|
|
1158
|
+
function Reviews() {
|
|
1159
|
+
const { data } = useLoader(ReviewsLoader);
|
|
1160
|
+
return <ReviewList items={data} />;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// in the server component:
|
|
1164
|
+
<Suspense fallback={<ReviewsSkeleton />}>
|
|
1165
|
+
<Reviews />
|
|
1166
|
+
</Suspense>;
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
The limits:
|
|
1170
|
+
|
|
1171
|
+
- **A click before its prefetch has answered waits for it, then fetches the
|
|
1172
|
+
deferred part.** The flag is there to keep expensive or slow work out of
|
|
1173
|
+
every prefetch, so that an API that is costly to call is not called for
|
|
1174
|
+
every link a visitor might click. A click that comes while the link's
|
|
1175
|
+
prefetch is still in flight reuses that prefetch, as it does without the
|
|
1176
|
+
flag, and sends the fill when the prefetch answers. The page shows no later
|
|
1177
|
+
than with no prefetch; the deferred part shows its fallback until the fill
|
|
1178
|
+
lands, and can complete later than a click with no prefetch, by up to what
|
|
1179
|
+
was left of the prefetch. That is the price of keeping the work out of the
|
|
1180
|
+
prefetch.
|
|
1181
|
+
- **A navigation started before the fill answers cancels it.** It is
|
|
1182
|
+
cancelled when the newer navigation starts, not when that one commits. If
|
|
1183
|
+
the newer navigation then ends without replacing the page (a redirect the
|
|
1184
|
+
client refuses, an action refetch that drops its response), the deferred
|
|
1185
|
+
part keeps its fallback until the next navigation. Every other ending
|
|
1186
|
+
replaces the page anyway.
|
|
1187
|
+
- **`navigate()` resolves after the fill has answered.** The click's
|
|
1188
|
+
navigation fills the deferred part itself, so `await navigate(url)` (and a
|
|
1189
|
+
`router.push()` you await) settles a round trip after the page committed.
|
|
1190
|
+
`useNavigation()` reads `idle` from the commit, with `isStreaming` true
|
|
1191
|
+
until the fill has streamed in.
|
|
1192
|
+
- **A boundary is required for a fallback.** With no `loading()` on the entry
|
|
1193
|
+
and no `<Suspense>` around the read, nothing can show a fallback, so React
|
|
1194
|
+
holds the page being left on screen until the fill returns.
|
|
1195
|
+
`useNavigation()` reads `state: "loading"` for that time, as on any
|
|
1196
|
+
navigation that is still streaming, so a progress bar shows, and the URL
|
|
1197
|
+
hooks (`useNavigation().location`) change with the page. The address bar
|
|
1198
|
+
has already changed: it moves with the click, where a click with no
|
|
1199
|
+
prefetch moves it when its response starts. There is no definition-time
|
|
1200
|
+
error.
|
|
1201
|
+
- **`ctx.rendered()` is refused.** A loader that can be deferred throws on
|
|
1202
|
+
every request kind (a document load included, so you see it the first time
|
|
1203
|
+
you open the page in development) if it calls `await ctx.rendered()`. The
|
|
1204
|
+
fill does not re-run the entry's handler, so there is no render to wait for
|
|
1205
|
+
and no handle data to read. Remove `{ prefetch: false }` from the loader, or
|
|
1206
|
+
stop calling `ctx.rendered()`. Pushing a handle from the loader is fine.
|
|
1207
|
+
- **`ctx.set()` values from held handlers are not visible.** The fill runs the
|
|
1208
|
+
deferred work and middleware only. A handler or layout the client already
|
|
1209
|
+
holds does not run again, so a variable it set with `ctx.set()` is missing
|
|
1210
|
+
when the loader reads it with `ctx.get()`. Variables set by middleware are
|
|
1211
|
+
there, as middleware runs on every request; so are `ctx.params` and
|
|
1212
|
+
`ctx.request`.
|
|
1213
|
+
- **`redirect()`, `notFound()` and handle pushes arrive late.** They behave as
|
|
1214
|
+
for a loader that streams behind `loading()`, but they come with the fill,
|
|
1215
|
+
after the page is already on screen: a redirect becomes a replace
|
|
1216
|
+
navigation, `notFound()` renders the nearest not-found boundary, an error
|
|
1217
|
+
reaches the error boundary.
|
|
1218
|
+
- **The fill reads through the loader's own caching; a prefetch reads none
|
|
1219
|
+
of it, by intent.** A loader with `cache()` that nothing in the prefetch
|
|
1220
|
+
reads is skipped even when its entry is warm, and so is a `"use cache"`
|
|
1221
|
+
function it calls: a prefetch that looked would have to run the loader on
|
|
1222
|
+
a miss, which is the work the flag keeps out. The fill is where the loader
|
|
1223
|
+
runs. It serves a warm `cache()` entry without running the loader, runs
|
|
1224
|
+
the loader and stores on a cold one, and a `"use cache"` function inside
|
|
1225
|
+
the loader hits or stores as on any request.
|
|
1226
|
+
- **A loader something else reads with `ctx.use()` runs in the prefetch.**
|
|
1227
|
+
The reader needs the value, so the flag cannot keep the loader out. When
|
|
1228
|
+
the read is in time, the loader is an ordinary loader for that prefetch,
|
|
1229
|
+
its own `cache()` included: one run (none on a warm entry), the reader and
|
|
1230
|
+
the loader's data agree, nothing deferred, no fill for it. In time means:
|
|
1231
|
+
inside a handler that has no `loading()` (the router waits for it), or
|
|
1232
|
+
before the reader's first `await`.
|
|
1233
|
+
- **Two late readers make it run twice.** The loader runs in the prefetch for
|
|
1234
|
+
its reader and again in the fill, because by the time the read happens the
|
|
1235
|
+
loader's own segment has already been sent as deferred:
|
|
1236
|
+
|
|
1237
|
+
```tsx
|
|
1238
|
+
// 1. A handler under loading() is streamed: the router does not wait for
|
|
1239
|
+
// it, so a read after its first await comes late.
|
|
1240
|
+
path(
|
|
1241
|
+
"/product/:id",
|
|
1242
|
+
async (ctx) => {
|
|
1243
|
+
const product = await getProduct(ctx.params.id);
|
|
1244
|
+
const reviews = await ctx.use(ReviewsLoader); // late: runs it here
|
|
1245
|
+
return <Product product={product} reviews={reviews} />;
|
|
1246
|
+
},
|
|
1247
|
+
{ name: "product" },
|
|
1248
|
+
() => [
|
|
1249
|
+
loader(ReviewsLoader, { prefetch: false }), // and the fill runs it again
|
|
1250
|
+
loading(<ProductSkeleton />),
|
|
1251
|
+
],
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// 2. A loader that is not deferred itself and reads it after an await.
|
|
1255
|
+
const SummaryLoader = createLoader(async (ctx) => {
|
|
1256
|
+
const product = await getProduct(ctx.params.id);
|
|
1257
|
+
const reviews = await ctx.use(ReviewsLoader); // late: two runs
|
|
1258
|
+
return summarize(product, reviews);
|
|
1259
|
+
});
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
Data is never missing, only fetched twice, and the page shows the fill's
|
|
1263
|
+
value. To get one run, start the read before the first `await`
|
|
1264
|
+
(`const reviews = ctx.use(ReviewsLoader)` at the top, awaited where you
|
|
1265
|
+
need it), or drop the flag from a loader the page cannot render without.
|
|
1266
|
+
|
|
1267
|
+
- **`clientUrls()` routes take it on `loader()`.** In a `clientUrls()`
|
|
1268
|
+
definition `loader(Def, { prefetch: false })` works as here: a click
|
|
1269
|
+
inside the group shows the destination at once, commits the prefetched
|
|
1270
|
+
data with the URL, and one fill runs the flagged loaders (see
|
|
1271
|
+
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1272
|
+
on a group route, from the group or from a server layout above it.
|
|
1273
|
+
- **No effect inside `intercept()`.** Its loaders accept the flag and ignore
|
|
1274
|
+
it, in `urls()` and in `clientUrls()`: a prefetch that resolves an
|
|
1275
|
+
intercept defers nothing.
|
|
1276
|
+
- **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
|
|
1277
|
+
bake lane and is served from the shell, so `prefetch: false` is ignored for
|
|
1278
|
+
it and development logs a warning. Under `cache()` and `Prerender` the flag
|
|
1279
|
+
works as above (see `/caching`, `/prerender`, `/ppr`).
|
|
1280
|
+
|
|
1281
|
+
To skip a handler along with its loaders, flag the `loading()` instead (see
|
|
1282
|
+
`/route` → "`loading()` options"). `loader(Def, { prefetch: false })` accepts
|
|
1283
|
+
only a boolean; anything else throws `loader() prefetch must be a boolean`.
|
|
1284
|
+
|
|
1285
|
+
Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
|
|
1286
|
+
and `result.readDeferred()` (see `/testing`).
|
|
1287
|
+
|
|
1097
1288
|
## Fetchable Loaders
|
|
1098
1289
|
|
|
1099
1290
|
By default, loaders only run as part of a render (document, navigation, or
|
|
@@ -850,8 +850,8 @@ For **dashboard / admin / settings-shaped sections** — high navigation
|
|
|
850
850
|
frequency inside one layout, mostly tab/param/filter switches — also consider
|
|
851
851
|
porting that route group to `clientUrls()` (`/client-urls`): the definition
|
|
852
852
|
matches in the browser (instant optimistic pending, no server round-trip to
|
|
853
|
-
start a transition) and
|
|
854
|
-
switches that don't invalidate it, which is the fastest transition shape Rango
|
|
853
|
+
start a transition) and per-loader `revalidate()` predicates (run on the
|
|
854
|
+
server) hold data across switches that don't invalidate it, which is the fastest transition shape Rango
|
|
855
855
|
has. Server-component routes and `clientUrls()` groups compose in one tree.
|
|
856
856
|
|
|
857
857
|
## 7. Server Actions
|