@rangojs/router 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/{chunk-4HHFQSDC.js → chunk-3CQXDX3A.js} +5 -5
- package/dist/bin/{client-urls-projection-4Q5EQDAL.js → client-urls-projection-2GBDM5YK.js} +1 -1
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-discovery-2ZTYRZ3V.js → runtime-discovery-PA3V3IFQ.js} +2 -2
- package/dist/bin/{temp-rsc-server-EUNKRFJD.js → temp-rsc-server-WDXEHDY7.js} +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +1 -1
- package/dist/types/client-urls/types.d.ts +6 -6
- package/dist/types/decode-loader-results.d.ts +1 -1
- package/dist/types/errors.d.ts +3 -3
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/loader-store.d.ts +20 -8
- package/dist/types/outlet-context.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +5 -5
- package/dist/types/router/loader-resolution.d.ts +1 -1
- package/dist/types/router/match-context.d.ts +12 -0
- package/dist/types/router/navigation-snapshot.d.ts +17 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +7 -7
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
- package/dist/types/server/context.d.ts +3 -3
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +1 -1
- package/dist/types/ssr/ssr-root.d.ts +1 -1
- package/dist/types/ssr-suspension-warning.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +19 -19
- package/dist/types/types/segments.d.ts +2 -2
- package/dist/types/urls/path-helper-types.d.ts +4 -4
- package/dist/types/urls/pattern-types.d.ts +1 -1
- package/dist/types/vite/plugin-types.d.ts +1 -1
- package/dist/vite/index.js +7 -7
- package/package.json +1 -1
- package/skills/app-structure/e-commerce.md +1 -1
- package/skills/breadcrumbs/SKILL.md +5 -5
- package/skills/cache-guide/SKILL.md +4 -4
- package/skills/caching/SKILL.md +1 -1
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +20 -20
- package/skills/comparison/references/framework-comparison.md +6 -6
- package/skills/defer-hydration/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +5 -5
- package/skills/hooks/data.md +5 -2
- package/skills/hooks/handle-and-actions.md +9 -9
- package/skills/loader/SKILL.md +52 -47
- package/skills/middleware/SKILL.md +2 -2
- package/skills/migrate-nextjs/SKILL.md +10 -10
- package/skills/migrate-react-router/SKILL.md +1 -1
- package/skills/migrate-react-router/component-migration.md +2 -2
- package/skills/migrate-react-router/data-and-actions.md +2 -2
- package/skills/migrate-react-router/route-mapping.md +2 -2
- package/skills/parallel/SKILL.md +2 -2
- package/skills/ppr/SKILL.md +91 -92
- package/skills/prerender/SKILL.md +5 -5
- package/skills/rango/SKILL.md +7 -7
- package/skills/route/SKILL.md +1 -1
- package/skills/router-setup/SKILL.md +2 -2
- package/skills/scripts/SKILL.md +3 -3
- package/skills/shell-manifest/SKILL.md +3 -3
- package/skills/testing/serve-shell-request.md +1 -1
- package/skills/theme/SKILL.md +2 -2
- package/skills/typesafety/env-and-bindings.md +2 -2
- package/skills/use-cache/SKILL.md +1 -1
- package/skills/ux-architecture/SKILL.md +7 -7
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +1 -1
- package/src/client-urls/client-urls.ts +16 -9
- package/src/client-urls/server-projection.ts +4 -4
- package/src/client-urls/types.ts +6 -6
- package/src/decode-loader-results.ts +1 -1
- package/src/errors.ts +9 -9
- package/src/handles/meta.ts +2 -2
- package/src/loader-store.ts +29 -13
- package/src/outlet-context.ts +1 -1
- package/src/route-definition/dsl-helpers.ts +4 -8
- package/src/route-definition/helpers-types.ts +5 -5
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/match-api.ts +2 -0
- package/src/router/match-context.ts +22 -1
- package/src/router/match-middleware/cache-lookup.ts +17 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -3
- package/src/router/navigation-snapshot.ts +39 -8
- package/src/router/segment-resolution/loader-cache.ts +8 -8
- package/src/router/segment-resolution/prefetch-deferral.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +3 -3
- package/src/router/types.ts +1 -1
- package/src/rsc/shell-capture-constants.ts +3 -3
- package/src/rsc/shell-capture.ts +12 -12
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +5 -5
- package/src/server/cookie-store.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +6 -6
- package/src/ssr/ssr-root.tsx +1 -1
- package/src/ssr-suspension-warning.ts +4 -4
- package/src/types/loader-types.ts +19 -19
- package/src/types/segments.ts +2 -2
- package/src/urls/path-helper-types.ts +4 -4
- package/src/urls/pattern-types.ts +1 -1
- package/src/use-loader.tsx +5 -3
- package/src/vite/discovery/shell-prerender-phase.ts +1 -1
- package/src/vite/plugin-types.ts +1 -1
|
@@ -153,10 +153,10 @@ export type PathHelpers<TEnv> = {
|
|
|
153
153
|
/**
|
|
154
154
|
* Attach a data loader to the current route/layout.
|
|
155
155
|
*
|
|
156
|
-
* Pass `{ ssr:
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
156
|
+
* Pass `{ ssr: "await" }` to await this loader before first flush on
|
|
157
|
+
* DOCUMENT requests (see {@link LoaderOptions}): the opt-in for loaders
|
|
158
|
+
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
159
|
+
* SSR'd HTML.
|
|
160
160
|
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` route the
|
|
161
161
|
* flag BAKES: the loader executes at shell capture and its settled return
|
|
162
162
|
* freezes into the shell (nested promises stay live holes) — the pre-flush
|
|
@@ -61,7 +61,7 @@ export interface PartialPrerenderProps {
|
|
|
61
61
|
* Upper bound (serialized UTF-8 bytes) on the loader pins in the capture
|
|
62
62
|
* data snapshot: the bake-lane loader records. The doc record (the handler
|
|
63
63
|
* layer every HIT replays) is exempt. A document HIT is served only from an
|
|
64
|
-
* entry that holds a pin for every `ssr:
|
|
64
|
+
* entry that holds a pin for every `ssr: "await"` loader its HTML baked, so
|
|
65
65
|
* over the cap no document shell is stored: documents render in full
|
|
66
66
|
* (MISS), and client navigations still replay the handler layer and run
|
|
67
67
|
* those loaders fresh. Reported once per key. Defaults to 8 MiB. The whole
|
|
@@ -166,7 +166,7 @@ interface RangoBaseOptions {
|
|
|
166
166
|
* which is why React outlines by default.
|
|
167
167
|
*
|
|
168
168
|
* When UNSET, document renders and PPR shell captures whose matched chain
|
|
169
|
-
* has a `loader(Def, { ssr:
|
|
169
|
+
* has a `loader(Def, { ssr: "await" })` entry auto-raise to MAX_SAFE_INTEGER —
|
|
170
170
|
* the loader was awaited before first flush precisely so its content ships
|
|
171
171
|
* in-place, and outlining the boundary it feeds would defeat that. A shell
|
|
172
172
|
* HIT inherits the captured value, so it matches the live render. Setting
|
package/dist/vite/index.js
CHANGED
|
@@ -4604,7 +4604,7 @@ function routerKeyPrefix(routerId2) {
|
|
|
4604
4604
|
|
|
4605
4605
|
// src/rsc/shell-capture-constants.ts
|
|
4606
4606
|
var SHELL_CAPTURE_MAX_WAIT_MS = 15e3;
|
|
4607
|
-
var PPR_LANE_HINT = 'Lane rule: only loader(Def, { ssr:
|
|
4607
|
+
var PPR_LANE_HINT = 'Lane rule: only loader(Def, { ssr: "await" }) executes at capture and bakes its non-promise data; every other loader is live (masked at capture, fresh per request) and needs loading() or an inline <Suspense> above its reader. See the /ppr skill, "The loader lane rule" (node_modules/@rangojs/router/skills/ppr/SKILL.md).';
|
|
4608
4608
|
|
|
4609
4609
|
// src/cache/cache-exec-scope.ts
|
|
4610
4610
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -4633,8 +4633,8 @@ var LOADER_BODY_SCOPE_KEY = /* @__PURE__ */ Symbol.for("rangojs-router:loader-bo
|
|
|
4633
4633
|
var loaderBodyScopeALS = globalThis[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage2();
|
|
4634
4634
|
function requestReadCaptureFix(what) {
|
|
4635
4635
|
return {
|
|
4636
|
-
capture: `The captured shell is served to every user of this URL, so request-scoped data read here would leak one user's ${what} to others. Read it inside a loader without ssr:
|
|
4637
|
-
warning:
|
|
4636
|
+
capture: `The captured shell is served to every user of this URL, so request-scoped data read here would leak one user's ${what} to others. Read it inside a loader without ssr: "await" and consume it with useLoader, e.g. createLoader(async () => getUser(cookies().get("session")?.value)). ${PPR_LANE_HINT}`,
|
|
4637
|
+
warning: 'Read it in a loader without ssr: "await" and consume it with useLoader under loading() or an inline <Suspense> (a live hole). A promise the handler passes or pushes does not help: the capture waits for it.'
|
|
4638
4638
|
};
|
|
4639
4639
|
}
|
|
4640
4640
|
var NON_CACHEABLE_READ = {
|
|
@@ -6074,7 +6074,7 @@ var RAW_COOKIES_READ = {
|
|
|
6074
6074
|
verb: "called",
|
|
6075
6075
|
fix: COOKIES_READ.fix
|
|
6076
6076
|
};
|
|
6077
|
-
var THEME_READ_FIX =
|
|
6077
|
+
var THEME_READ_FIX = 'On ppr and cache() routes, read the theme with useTheme() in a client component, or in a live loader (no ssr: "await") with cookies().get(<storageKey>). The <html> theme class needs neither: the theme script sets it before paint. See the /theme skill (node_modules/@rangojs/router/skills/theme/SKILL.md).';
|
|
6078
6078
|
var THEME_READ = {
|
|
6079
6079
|
verb: "read",
|
|
6080
6080
|
fix: {
|
|
@@ -6289,7 +6289,7 @@ function serializeRoute(route2, options, ordinals) {
|
|
|
6289
6289
|
});
|
|
6290
6290
|
const awaitedLoaderIndices = indicesWhere(
|
|
6291
6291
|
route2.loaders,
|
|
6292
|
-
({ ssr }) => ssr ===
|
|
6292
|
+
({ ssr }) => ssr === "await"
|
|
6293
6293
|
);
|
|
6294
6294
|
const transition3 = serializeTransition(route2);
|
|
6295
6295
|
return Object.freeze({
|
|
@@ -7344,7 +7344,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7344
7344
|
// package.json
|
|
7345
7345
|
var package_default = {
|
|
7346
7346
|
name: "@rangojs/router",
|
|
7347
|
-
version: "0.
|
|
7347
|
+
version: "0.35.0",
|
|
7348
7348
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7349
7349
|
keywords: [
|
|
7350
7350
|
"react",
|
|
@@ -14567,7 +14567,7 @@ async function runShellPrerenderPhase(s, builder, versionsTable) {
|
|
|
14567
14567
|
injectRSCPayload: htmlStream.injectRSCPayload,
|
|
14568
14568
|
headScripts: "preinit",
|
|
14569
14569
|
// Build-captured shells honor the explicit outlining budget, or the
|
|
14570
|
-
// ssr:
|
|
14570
|
+
// ssr: "await" auto-raise without one (createShellCaptureHandler): the
|
|
14571
14571
|
// capture value rides the postponed state into every runtime resume().
|
|
14572
14572
|
...s.opts?.progressiveChunkSize !== void 0 && {
|
|
14573
14573
|
progressiveChunkSize: s.opts.progressiveChunkSize
|
package/package.json
CHANGED
|
@@ -379,7 +379,7 @@ product) and the pages render entirely from loaders, mount the shop as a
|
|
|
379
379
|
`clientUrls()` group, `include("/shop", shopUrls, { name: "shop" })`:
|
|
380
380
|
navigations inside it render at the click, and each loader's `revalidate()`
|
|
381
381
|
holds its data across them. A group has no handlers, so a `ppr` group route
|
|
382
|
-
holds its client components' HTML and its `ssr:
|
|
382
|
+
holds its client components' HTML and its `ssr: "await"` loaders' data, and
|
|
383
383
|
every other loader is a hole; handler output baked from a tagged
|
|
384
384
|
`"use cache"` read, as in the catalog above, needs `urls()` routes. The router
|
|
385
385
|
repository's `tests/vite-rsc-demo/src/urls/client-shop.client.tsx` is a full
|
|
@@ -102,11 +102,11 @@ Two pushes from one segment accumulate in push order (`Shop › product`).
|
|
|
102
102
|
Delivery follows the loader race model: a push that beats the handler barrier
|
|
103
103
|
is in the SSR'd document; a push after a slow fetch streams and applies
|
|
104
104
|
client-side (`useHandle` re-renders when it lands). To guarantee document
|
|
105
|
-
delivery, register the loader as `loader(Def, { ssr:
|
|
105
|
+
delivery, register the loader as `loader(Def, { ssr: "await" })` —
|
|
106
106
|
see `/loader` → "Writing Handles from Loaders". On a `ppr` route the race is
|
|
107
|
-
decided at capture: a loader without `ssr:
|
|
107
|
+
decided at capture: a loader without `ssr: "await"` never runs there, so its
|
|
108
108
|
crumb is not in the shell and applies client-side on every HIT; with
|
|
109
|
-
`ssr:
|
|
109
|
+
`ssr: "await"` the crumb bakes into the shell (`/ppr` → "On a shell HIT").
|
|
110
110
|
Under `cache()` loader crumbs are not stored with the cached segments; the
|
|
111
111
|
loader re-pushes them on every hit, so they appear once even without an `href`.
|
|
112
112
|
A loader with its own `cache()` keeps its crumbs on its own hits too: they are
|
|
@@ -147,7 +147,7 @@ in a `ppr` shell it is stored resolved: the cache write, or the shell capture
|
|
|
147
147
|
(within `ppr.captureTimeout`), waits for a `content` promise and for a
|
|
148
148
|
`.defer()` slot (below), and every HIT replays the crumb as it was resolved
|
|
149
149
|
then, without running the handler. For per-request crumb content, push it
|
|
150
|
-
from a loader without `ssr:
|
|
150
|
+
from a loader without `ssr: "await"` (see "Pushing from Loaders").
|
|
151
151
|
|
|
152
152
|
### Deferred content (decide now, resolve from a deep component)
|
|
153
153
|
|
|
@@ -211,7 +211,7 @@ swap in — no blank, no pending entry. The hold-then-swap is deliberate: a hand
|
|
|
211
211
|
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
212
212
|
`label`/`href` are unknown at handler time AND the producer is a component,
|
|
213
213
|
not a loader — a loader-produced crumb pushes directly from the loader body
|
|
214
|
-
(add `{ ssr:
|
|
214
|
+
(add `{ ssr: "await" }` if it must be in the SSR'd document), and if
|
|
215
215
|
you know `label`/`href` and only the `content` is async, push a concrete item
|
|
216
216
|
with a `Promise` `content` field instead (the `content` field is a nested
|
|
217
217
|
promise you resolve with `use()` in your component; no `.defer()` needed).
|
|
@@ -38,7 +38,7 @@ Quick rules:
|
|
|
38
38
|
CDN caching (`/deployment-caching`).
|
|
39
39
|
- A value that must be fresh on **every** request → a loader (never cached
|
|
40
40
|
unless you opt in), read with `useLoader()`; on a `ppr` route, keep it off
|
|
41
|
-
`ssr:
|
|
41
|
+
`ssr: "await"`. A handler inside a `cache()` or `ppr` boundary that awaits
|
|
42
42
|
`ctx.use(Loader)` renders the value it got on the miss, and every hit
|
|
43
43
|
replays that output.
|
|
44
44
|
|
|
@@ -161,7 +161,7 @@ The `ppr` shell capture has its own, stricter guard: `cookies()`, `headers()`,
|
|
|
161
161
|
`ctx.request.headers`, a theme read, a `{ cache: false }` variable read, and
|
|
162
162
|
`ctx.dynamic()` refuse the
|
|
163
163
|
capture anywhere it waits — handlers, promises they pass or push, async server
|
|
164
|
-
components, `ssr:
|
|
164
|
+
components, `ssr: "await"` loaders, and loaders a handler awaits (no loader
|
|
165
165
|
exemption there). The route then serves uncached; see `/ppr`.
|
|
166
166
|
|
|
167
167
|
### Cross-deploy safety: version-segmented store keys
|
|
@@ -490,7 +490,7 @@ So `"use cache: short"` (60s) inside `cache({ ttl: 600 })` yields ~600s freshnes
|
|
|
490
490
|
on hits, **not** 60s. This is not a bug: setting `cache({ ttl: 600 })` declares
|
|
491
491
|
"this subtree may be ~600s stale." **If a value must be fresher than its
|
|
492
492
|
enclosing segment, put it in a loader** read with `useLoader()` (without
|
|
493
|
-
`ssr:
|
|
493
|
+
`ssr: "await"` on a `ppr` route). `debugPerformance` prints cache hits per
|
|
494
494
|
layer, so the actual per-request behavior is observable.
|
|
495
495
|
|
|
496
496
|
Keys and tags cross the nesting too:
|
|
@@ -643,7 +643,7 @@ This means `cache()` gives you cached UI + fresh data by default, where the
|
|
|
643
643
|
data is read outside the cached output: `useLoader()` in a client component.
|
|
644
644
|
A handler inside the boundary that awaits `ctx.use(Loader)` renders the value
|
|
645
645
|
it got on the miss, and every hit replays that output. On a `ppr` route an
|
|
646
|
-
`ssr:
|
|
646
|
+
`ssr: "await"` loader is the other exception: it bakes into the shell (`/ppr`).
|
|
647
647
|
To also cache a loader's data, explicitly opt in with
|
|
648
648
|
`loader(Fn, () => [cache({...})])`.
|
|
649
649
|
That entry is keyed by loader, host, path and params, not by the route's
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -707,7 +707,7 @@ times out, or whose snapshot is truncated or corrupt (also evicted), cannot
|
|
|
707
707
|
replay the captured segment record, so it degrades: the entry is replaced and
|
|
708
708
|
recaptured and the page reloads once into a cache-miss render (`/ppr`). The
|
|
709
709
|
snapshot holds only what a HIT reads: every HIT replays the handler layer from
|
|
710
|
-
the captured segment record and serves an `ssr:
|
|
710
|
+
the captured segment record and serves an `ssr: "await"` loader's baked
|
|
711
711
|
container, so no `"use cache"` or loader `cache()` item is stored in it. A
|
|
712
712
|
live hole, and any loader that runs on the HIT, reads those from the store
|
|
713
713
|
under their own ttl/swr and tags.
|
package/skills/catalog.json
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "client-urls",
|
|
44
|
-
"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
|
|
44
|
+
"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:\"await\" 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.",
|
|
45
45
|
"argumentHint": "[setup]",
|
|
46
46
|
"path": "skills/client-urls/SKILL.md"
|
|
47
47
|
},
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "loader",
|
|
146
|
-
"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
|
|
146
|
+
"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:\"await\"), or must stay out of link prefetches (prefetch:false).",
|
|
147
147
|
"argumentHint": "[loader]",
|
|
148
148
|
"path": "skills/loader/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, server-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:
|
|
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:"await" 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
|
|
|
@@ -132,7 +132,7 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
|
|
|
132
132
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
133
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()` |
|
|
134
134
|
| `layout()` | `layout(Component, () => [...])` — children are required and must contain at least one `path()`; may also hold `loader()`, `loading()`, `intercept()`, nested `layout()` |
|
|
135
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr:
|
|
135
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: "await", prefetch: false \| "intent" }, use?)` — see below |
|
|
136
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 |
|
|
137
137
|
| `revalidate()` | Valid **inside a loader() use callback only**; runs on the server (the build hoists it out of the `"use client"` module) |
|
|
138
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 |
|
|
@@ -185,11 +185,11 @@ export default clientUrls(({ path }) => [
|
|
|
185
185
|
]);
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
| Component mounted by | Its `.use` may return
|
|
189
|
-
| -------------------- |
|
|
190
|
-
| `path()` | `loader()`, `loading()`, `transition()`
|
|
191
|
-
| `layout()` | `loader()`, `loading()`
|
|
192
|
-
| `intercept()` | `loader()` (without `ssr:
|
|
188
|
+
| Component mounted by | Its `.use` may return |
|
|
189
|
+
| -------------------- | ------------------------------------------------ |
|
|
190
|
+
| `path()` | `loader()`, `loading()`, `transition()` |
|
|
191
|
+
| `layout()` | `loader()`, `loading()` |
|
|
192
|
+
| `intercept()` | `loader()` (without `ssr: "await"`), `loading()` |
|
|
193
193
|
|
|
194
194
|
- **Merge order is the server's.** The component's items come first and the
|
|
195
195
|
mount site's own items second: an explicit `loading()` or `transition()`
|
|
@@ -511,7 +511,7 @@ The next action or navigation that reruns the loader replaces the message.
|
|
|
511
511
|
|
|
512
512
|
A failure returned as data is an ordinary value, so whatever stores the
|
|
513
513
|
loader's value stores it too: a `cache()` on the loader serves it for its TTL,
|
|
514
|
-
a `ppr` shell bakes an `ssr:
|
|
514
|
+
a `ppr` shell bakes an `ssr: "await"` loader's value, and a prerender stores it
|
|
515
515
|
at build time. A thrown error is never stored (a stale entry keeps serving).
|
|
516
516
|
To cache the expensive part, cache the call the loader makes (`"use cache"` on
|
|
517
517
|
that function: a call that throws stores nothing) and turn the throw into
|
|
@@ -545,7 +545,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
545
545
|
On a document load, `notFound()` streams the resolved not-found UI in the
|
|
546
546
|
envelope and _opportunistically_ sets a real 404 status — the status write only
|
|
547
547
|
wins if the rejection settles before the document Response is constructed (a
|
|
548
|
-
fast, pre-fetch existence check usually wins; see `ssr:
|
|
548
|
+
fast, pre-fetch existence check usually wins; see `ssr: "await"` below
|
|
549
549
|
for the deterministic version). On navigations the 404 UI swaps in with the
|
|
550
550
|
URL preserved. A loader `redirect()` is always a client-side navigate — there
|
|
551
551
|
is no document-lane 302 from loaders; pre-stream redirect authority belongs to
|
|
@@ -557,10 +557,10 @@ push. Reads moved to `ctx.get(handle)`, gated behind `await ctx.rendered()`.
|
|
|
557
557
|
Delivery follows the race model: pushes that settle before the handler barrier
|
|
558
558
|
ride the SSR handle snapshot; later ones stream and apply post-hydration
|
|
559
559
|
(document loads) or progressively (navigations). On a `ppr` shell HIT a loader
|
|
560
|
-
without `ssr:
|
|
560
|
+
without `ssr: "await"` never ran during the capture, so its pushes are never in
|
|
561
561
|
the shell and always apply client-side. See `/loader` for the full contract.
|
|
562
562
|
|
|
563
|
-
## `ssr:
|
|
563
|
+
## `ssr: "await"` — the SSR-completeness opt-in
|
|
564
564
|
|
|
565
565
|
By default every loader streams on every render, so nothing a slow loader
|
|
566
566
|
produces is _guaranteed_ to be in the SSR'd HTML — data SSRs as the Suspense
|
|
@@ -570,7 +570,7 @@ document — `<head>` meta via a handle, or a real 404 status — flag it:
|
|
|
570
570
|
|
|
571
571
|
```ts
|
|
572
572
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
573
|
-
loader(ProductLoader, { ssr:
|
|
573
|
+
loader(ProductLoader, { ssr: "await" }, () => [
|
|
574
574
|
revalidate(({ currentParams, nextParams, defaultShouldRevalidate }) =>
|
|
575
575
|
currentParams.slug !== nextParams.slug ? defaultShouldRevalidate : false,
|
|
576
576
|
),
|
|
@@ -579,9 +579,9 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
579
579
|
]),
|
|
580
580
|
```
|
|
581
581
|
|
|
582
|
-
The
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
The value says what the server render does with the loader: `"stream"` (the
|
|
583
|
+
default) streams it, `"await"` awaits it. Document renders await a flagged
|
|
584
|
+
loader before first flush (data settled — no fallback
|
|
585
585
|
paints; handle pushes beat the barrier snapshot; a thrown `notFound()` is a
|
|
586
586
|
deterministic real 404, no warm-up race); client navigations stream exactly as
|
|
587
587
|
before. Scoped per loader — the flagged loader awaits only itself, siblings
|
|
@@ -613,12 +613,12 @@ Pinned end to end by dev + production e2e in the router repository.
|
|
|
613
613
|
ships its fallback and swaps the content in with a script, which a browser
|
|
614
614
|
without JavaScript never runs. That includes a client component a loader's
|
|
615
615
|
answer draws under an inline `<Suspense>`: a fast loader's answer usually
|
|
616
|
-
makes the first flush, and `ssr:
|
|
616
|
+
makes the first flush, and `ssr: "await"` guarantees the loader's data does. It
|
|
617
617
|
does not cover the client component's module: the first document a build's
|
|
618
618
|
fresh server renders can still be waiting for it. The dev server matches a
|
|
619
619
|
build here: when the shell is ready, it waits for the client components the
|
|
620
620
|
payload has named and that are still loading, and for nothing else. On a
|
|
621
|
-
document with an `ssr:
|
|
621
|
+
document with an `ssr: "await"` loader, a part React finishes a moment after the
|
|
622
622
|
shell, such as a `useActionState` form (React suspends it once to encode its
|
|
623
623
|
action), is in the first flush too: the router gives React up to three more
|
|
624
624
|
turns before it, and none when the document is already complete.
|
|
@@ -665,7 +665,7 @@ rest of the destination; otherwise it is held. A same-route click
|
|
|
665
665
|
prefetch of the intercept's target defers it, and the click opens the
|
|
666
666
|
modal at once with one fill for it; a hover's runs a loader marked
|
|
667
667
|
`"intent"`.
|
|
668
|
-
- With `ssr:
|
|
668
|
+
- With `ssr: "await"` on a `ppr` group route the flag works as on a server
|
|
669
669
|
route: deferred unless the prefetch serves the loader from a pin of a
|
|
670
670
|
document-captured shell (`/ppr` → "`prefetch: false` on a `ppr` route").
|
|
671
671
|
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
@@ -726,7 +726,7 @@ a hard load of the target URL renders the full route.
|
|
|
726
726
|
matches) never renders as a group route: the server ships the competing
|
|
727
727
|
routes with the group.
|
|
728
728
|
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
729
|
-
kick off in parallel, so awaiting one (`ssr:
|
|
729
|
+
kick off in parallel, so awaiting one (`ssr: "await"`) gives
|
|
730
730
|
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
731
731
|
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
732
732
|
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
@@ -786,7 +786,7 @@ The router repository (not shipped in this package) carries a canonical
|
|
|
786
786
|
consumer at `tests/vite-rsc-demo/src/urls/client-shop.client.tsx`: layout +
|
|
787
787
|
index + product routes, param-sensitive predicates, action-scoped cart
|
|
788
788
|
revalidation, loader-thrown `notFound()`/`redirect()`, loader-written
|
|
789
|
-
Meta/Breadcrumbs, viewport prefetch on cards, and the `ssr:
|
|
789
|
+
Meta/Breadcrumbs, viewport prefetch on cards, and the `ssr: "await"`
|
|
790
790
|
fixtures — with e2e suites next to it pinning each contract in dev and
|
|
791
791
|
production. The sections above are self-contained; the app is corroboration,
|
|
792
792
|
not required reading.
|
|
@@ -638,17 +638,17 @@ this resource-aware policy layer.
|
|
|
638
638
|
|
|
639
639
|
`createLoader(fn)` / `loader()` define **live-by-default** data units: they are
|
|
640
640
|
excluded from an enclosing segment cache and resolve on every request unless the
|
|
641
|
-
loader itself explicitly opts into `cache()`. A loader without `ssr:
|
|
641
|
+
loader itself explicitly opts into `cache()`. A loader without `ssr: "await"` may
|
|
642
642
|
safely read `cookies()`, `headers()`, request context, and `env` because its
|
|
643
643
|
execution is outside the cached shell; a loader with its own `cache()` must put
|
|
644
644
|
those reads in its `key()`, or its miss fails and stores nothing. On a `ppr`
|
|
645
|
-
route an `ssr:
|
|
645
|
+
route an `ssr: "await"` loader bakes into the shell, so an identity read there
|
|
646
646
|
refuses the capture. Loaders run in parallel, stream independently under
|
|
647
647
|
`loading()` boundaries, compose server-side via `ctx.use(OtherLoader)`, and
|
|
648
648
|
carry route-level authority: they can throw `notFound()`/`redirect()`, WRITE
|
|
649
649
|
handles (`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the
|
|
650
650
|
data's producer), read handle data after the render settles (`ctx.get(handle)`
|
|
651
|
-
behind `await ctx.rendered()`), and opt into `loader(Def, { ssr:
|
|
651
|
+
behind `await ctx.rendered()`), and opt into `loader(Def, { ssr: "await" })` so a
|
|
652
652
|
document render awaits them before first flush (deterministic SSR'd data, meta,
|
|
653
653
|
and 404 status). Reads happen through `useLoader` in a client component
|
|
654
654
|
(including its SSR pass) or `useFetchLoader` for standalone client fetches.
|
|
@@ -715,12 +715,12 @@ that no loader-before-render has:
|
|
|
715
715
|
|
|
716
716
|
- it is the **cache-safety escape hatch** — the only place request-coupled reads
|
|
717
717
|
are allowed inside a cache scope, because it is the part guaranteed to re-run
|
|
718
|
-
(a loader without `ssr:
|
|
718
|
+
(a loader without `ssr: "await"` and without its own unkeyed `cache()`);
|
|
719
719
|
- it is **standalone and composable**, not route-coupled — one `createLoader()`
|
|
720
720
|
read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
|
|
721
721
|
- it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
|
|
722
722
|
`loading()`, so the shell never blocks on it (and gating is a deliberate
|
|
723
|
-
per-loader opt-in — `{ ssr:
|
|
723
|
+
per-loader opt-in — `{ ssr: "await" }` awaits ONE loader on document
|
|
724
724
|
renders while the rest keep streaming — not the model's default);
|
|
725
725
|
- it is **client-addressable and refreshable** independent of navigation (`key`,
|
|
726
726
|
`refreshGroup`, `useRefreshLoaders`), behaving like a built-in, server-defined
|
|
@@ -798,7 +798,7 @@ shell.
|
|
|
798
798
|
|
|
799
799
|
This is where the taint and loader designs meet. Loaders are the sanctioned
|
|
800
800
|
dynamic holes: they run outside the enclosing segment cache and may read the
|
|
801
|
-
current request (except, on a `ppr` route, an `ssr:
|
|
801
|
+
current request (except, on a `ppr` route, an `ssr: "await"` loader or a loader a
|
|
802
802
|
handler awaits, which the capture runs and refuses on such a read), while the
|
|
803
803
|
shared shell remains protected. Next.js also isolates runtime APIs from ordinary
|
|
804
804
|
`"use cache"`, and React's optional taint APIs protect values at the
|
|
@@ -121,8 +121,8 @@ payload from that record (`settleCaptureRecord` in `src/rsc/shell-capture.ts`).
|
|
|
121
121
|
A boundary whose content is only handler output is therefore expected to bake
|
|
122
122
|
its content as a completed boundary instead of postponing. What still
|
|
123
123
|
postpones at capture is a boundary over live data: a loader without
|
|
124
|
-
`ssr:
|
|
125
|
-
`<Suspense>`, or a promise nested in an `ssr:
|
|
124
|
+
`ssr: "await"` read with `useLoader` under `loading()` or an inline
|
|
125
|
+
`<Suspense>`, or a promise nested in an `ssr: "await"` loader's value. Run the
|
|
126
126
|
prelude check in "Verifying and measuring" on your page rather than assuming
|
|
127
127
|
either shape.
|
|
128
128
|
|
|
@@ -417,11 +417,11 @@ The composition rules are this skill's: the component's items first, the
|
|
|
417
417
|
mount site's explicit items second, `loading()` and `transition()` last-wins,
|
|
418
418
|
loaders accumulate. What differs:
|
|
419
419
|
|
|
420
|
-
| Mount site in `clientUrls()` | The component's `.use` may return
|
|
421
|
-
| ---------------------------- |
|
|
422
|
-
| `path()` | `loader`, `loading`, `transition`
|
|
423
|
-
| `layout()` | `loader`, `loading`
|
|
424
|
-
| `intercept()` | `loader` (without `ssr:
|
|
420
|
+
| Mount site in `clientUrls()` | The component's `.use` may return |
|
|
421
|
+
| ---------------------------- | -------------------------------------------- |
|
|
422
|
+
| `path()` | `loader`, `loading`, `transition` |
|
|
423
|
+
| `layout()` | `loader`, `loading` |
|
|
424
|
+
| `intercept()` | `loader` (without `ssr: "await"`), `loading` |
|
|
425
425
|
|
|
426
426
|
- No `route`, `layout`, `intercept` or `include` items: a group's routes and
|
|
427
427
|
its generated types come from the builder's own items.
|
package/skills/hooks/data.md
CHANGED
|
@@ -28,7 +28,7 @@ boundary (or the route's `loading()`) — it does NOT render with
|
|
|
28
28
|
`isLoading: true`. Put a boundary above every read whose loader can be slow;
|
|
29
29
|
`isLoading` covers later refetches (`load()`, key/group refreshes). Once the
|
|
30
30
|
component renders, `data` is present. (On document loads a loader registered
|
|
31
|
-
with `{ ssr:
|
|
31
|
+
with `{ ssr: "await" }` is already settled at first paint, so its
|
|
32
32
|
reads never suspend there — see `/loader`.)
|
|
33
33
|
|
|
34
34
|
**`isLoading` covers navigations and `load()` only.** When a server action
|
|
@@ -261,7 +261,10 @@ tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
|
261
261
|
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
262
262
|
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
263
263
|
private bucket, so a group refresh updates only that read and never leaks into
|
|
264
|
-
unrelated unkeyed reads of the same loader.
|
|
264
|
+
unrelated unkeyed reads of the same loader. That private bucket lives as long as
|
|
265
|
+
its read is mounted, and a hidden React `<Activity>` counts as unmounted: when it
|
|
266
|
+
shows again, the read is back on the route's data until the next refresh. To keep
|
|
267
|
+
a refreshed value across an `<Activity>` hide, give the read a `key`. A bucket may belong to several groups
|
|
265
268
|
at once (one read tagged with multiple names, or different reads tagging the same
|
|
266
269
|
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
267
270
|
`key` — a plain-GET group refresh sends no params.
|
|
@@ -33,9 +33,9 @@ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
|
33
33
|
Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
34
34
|
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
35
|
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
|
-
`loader(Def, { ssr:
|
|
37
|
-
document. On a `ppr` shell HIT, a loader without `ssr:
|
|
38
|
-
the capture, so its pushes always apply client-side; with `ssr:
|
|
36
|
+
`loader(Def, { ssr: "await" })` makes them deterministic in the
|
|
37
|
+
document. On a `ppr` shell HIT, a loader without `ssr: "await"` never ran during
|
|
38
|
+
the capture, so its pushes always apply client-side; with `ssr: "await"` they
|
|
39
39
|
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
40
40
|
re-renders on every handle update, late loader pushes included.
|
|
41
41
|
|
|
@@ -46,12 +46,12 @@ belongs to the segment that pushed it and reaches `useHandle` when that push
|
|
|
46
46
|
settles. The page never waits for a handle, and a handle never shows the next
|
|
47
47
|
page's values over the page being left:
|
|
48
48
|
|
|
49
|
-
| Case | What `useHandle` returns
|
|
50
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
-
| Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. On a `ppr` shell HIT that holds for the handlers' and the `ssr:
|
|
52
|
-
| Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them
|
|
53
|
-
| Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land right after the page's commit: without view transitions in the same task, before paint; with view transitions when the transition's `ready` resolves
|
|
54
|
-
| `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page
|
|
49
|
+
| Case | What `useHandle` returns |
|
|
50
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. On a `ppr` shell HIT that holds for the handlers' and the `ssr: "await"` loaders' pushes, which the shell baked; a live-lane loader's push arrives right after hydration (see `/ppr`). |
|
|
52
|
+
| Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
|
|
53
|
+
| Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land right after the page's commit: without view transitions in the same task, before paint; with view transitions when the transition's `ready` resolves |
|
|
54
|
+
| `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
|
|
55
55
|
|
|
56
56
|
You never see a pending value, a blank, or the destination's title over the old
|
|
57
57
|
page. A crumb whose `content` is a promise read with `use()` in `<Suspense>`
|