@rangojs/router 0.34.1 → 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-RY6NMQM3.js → runtime-discovery-PA3V3IFQ.js} +2 -2
- package/dist/bin/{temp-rsc-server-F7AHOPV4.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 +1 -1
- 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/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 +1 -1
- 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 +1 -1
- 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/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/vite/discovery/shell-prerender-phase.ts +1 -1
- package/src/vite/plugin-types.ts +1 -1
|
@@ -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
|
|
@@ -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>`
|
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, must be settled in the SSR'd document (ssr:
|
|
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:"await"), or must stay out of link prefetches (prefetch:false).
|
|
4
4
|
argument-hint: "[loader]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ argument-hint: "[loader]"
|
|
|
8
8
|
|
|
9
9
|
A loader is a server function created with `createLoader()` and registered on a
|
|
10
10
|
route segment with `loader()`. It runs fresh on every request (on a `ppr` route,
|
|
11
|
-
one registered with `ssr:
|
|
11
|
+
one registered with `ssr: "await"` is baked into the shell instead), streams its
|
|
12
12
|
result to the client, and client components read it with `useLoader()`. This
|
|
13
13
|
skill covers defining, registering, consuming, revalidating, caching, and
|
|
14
14
|
client-fetching loaders.
|
|
@@ -130,15 +130,15 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
130
130
|
DSL loaders are the **live data layer**: they resolve fresh on every request,
|
|
131
131
|
even when the route is inside a `cache()` boundary, so `cache()` gives you
|
|
132
132
|
cached UI + fresh data by default. See "Loaders: The Live Data Layer" below.
|
|
133
|
-
The exception is a `ppr` route's `ssr:
|
|
133
|
+
The exception is a `ppr` route's `ssr: "await"` loader: it bakes into the shell,
|
|
134
134
|
and a shell HIT runs it only when its return carries promises.
|
|
135
135
|
|
|
136
136
|
### Cache safety
|
|
137
137
|
|
|
138
|
-
A DSL loader without `ssr:
|
|
138
|
+
A DSL loader without `ssr: "await"` can read `createVar({ cache: false })`
|
|
139
139
|
variables: it runs on every request, `cache()` and PPR shell HITs included, so
|
|
140
140
|
the read guard is bypassed for it. On a `ppr` route the capture runs an
|
|
141
|
-
`ssr:
|
|
141
|
+
`ssr: "await"` loader, and a loader a handler awaits, and bakes their values into
|
|
142
142
|
the shell: a `cookies()`, `headers()`, `ctx.request.headers` or
|
|
143
143
|
`{ cache: false }` read there refuses the capture, so the route keeps
|
|
144
144
|
rendering without a shell (warned once per key).
|
|
@@ -191,7 +191,7 @@ the tree).
|
|
|
191
191
|
- The same holds under a PPR shell capture (`/ppr`): handler consumption is the
|
|
192
192
|
BAKED lane — the loader executes at capture and the rendered value is a
|
|
193
193
|
capture-time copy served on every HIT (a HIT never runs the handler);
|
|
194
|
-
`useLoader` of a loader without `ssr:
|
|
194
|
+
`useLoader` of a loader without `ssr: "await"` is the live lane (an `ssr: "await"`
|
|
195
195
|
loader is the bake lane however it is read). Unlike `cache()`, an identity
|
|
196
196
|
read inside that loader (`cookies()`, `headers()`, `ctx.request.headers`, a
|
|
197
197
|
`{ cache: false }` variable) refuses the capture, so the route stays
|
|
@@ -226,26 +226,26 @@ never sends it (see `/hooks` `state.md`, "State on a document response").
|
|
|
226
226
|
|
|
227
227
|
### Full field surface
|
|
228
228
|
|
|
229
|
-
| Field | Type | Notes
|
|
230
|
-
| ------------------ | ------------------------------- |
|
|
231
|
-
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`.
|
|
232
|
-
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden.
|
|
233
|
-
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort).
|
|
234
|
-
| `url` | `URL` | Request URL with internal `_rsc*` transport params stripped. Use this for application logic.
|
|
235
|
-
| `originalUrl` | `URL` | Request URL with every param intact, including `_rsc*`. Only for debugging or custom cache keying.
|
|
236
|
-
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`).
|
|
237
|
-
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`.
|
|
238
|
-
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise.
|
|
239
|
-
| `env` | `any` | Bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). `createLoader` types it as `any`; annotate or cast at the use site.
|
|
240
|
-
| `waitUntil` | `(fn: () => Promise<void>)` | Run work after the response is sent. Takes a function, not a promise. Delegates to Cloudflare's `waitUntil`; fire-and-forget with error logging elsewhere.
|
|
241
|
-
| `executionContext` | `ExecutionContext \| undefined` | Raw Cloudflare `ExecutionContext` for libraries that need it; `undefined` off Cloudflare. Prefer `waitUntil`.
|
|
242
|
-
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`.
|
|
243
|
-
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise), or WRITE a handle: `ctx.use(Meta)({ title })` returns the push function — handler parity. Reads moved to `get`.
|
|
244
|
-
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. Not with `ssr:
|
|
245
|
-
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders.
|
|
246
|
-
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls.
|
|
247
|
-
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission.
|
|
248
|
-
| `reverse` | `AutofillAwareReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers; params the matched route carries are optional).
|
|
229
|
+
| Field | Type | Notes |
|
|
230
|
+
| ------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
231
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
232
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
233
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
234
|
+
| `url` | `URL` | Request URL with internal `_rsc*` transport params stripped. Use this for application logic. |
|
|
235
|
+
| `originalUrl` | `URL` | Request URL with every param intact, including `_rsc*`. Only for debugging or custom cache keying. |
|
|
236
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
237
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
238
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
239
|
+
| `env` | `any` | Bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). `createLoader` types it as `any`; annotate or cast at the use site. |
|
|
240
|
+
| `waitUntil` | `(fn: () => Promise<void>)` | Run work after the response is sent. Takes a function, not a promise. Delegates to Cloudflare's `waitUntil`; fire-and-forget with error logging elsewhere. |
|
|
241
|
+
| `executionContext` | `ExecutionContext \| undefined` | Raw Cloudflare `ExecutionContext` for libraries that need it; `undefined` off Cloudflare. Prefer `waitUntil`. |
|
|
242
|
+
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`. |
|
|
243
|
+
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise), or WRITE a handle: `ctx.use(Meta)({ title })` returns the push function — handler parity. Reads moved to `get`. |
|
|
244
|
+
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. Not with `ssr: "await"` (cycle; throws). |
|
|
245
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
246
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
247
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
248
|
+
| `reverse` | `AutofillAwareReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers; params the matched route carries are optional). |
|
|
249
249
|
|
|
250
250
|
### Example
|
|
251
251
|
|
|
@@ -556,14 +556,14 @@ served from cache. Route-level `cache()` caches rendered segments but never
|
|
|
556
556
|
loader data: loaders are excluded when the segments are stored and re-resolved
|
|
557
557
|
when they are served. Caching a loader's own data is a separate opt-in (see
|
|
558
558
|
"Opting a Loader into Caching"). A `ppr` shell does store some loader data: an
|
|
559
|
-
`ssr:
|
|
560
|
-
loader only when its return carries promises (see "`ssr:
|
|
559
|
+
`ssr: "await"` loader's settled data bakes into the shell, and a HIT runs that
|
|
560
|
+
loader only when its return carries promises (see "`ssr: "await"`" below and
|
|
561
561
|
`/ppr`).
|
|
562
562
|
|
|
563
563
|
Pre-rendering follows the same rule: the build-time Flight payload skips
|
|
564
564
|
loaders (there is no real request context), and at runtime the worker resolves
|
|
565
565
|
them fresh against the live database. The exception is a `Prerender` + `ppr`
|
|
566
|
-
route: its build-time shell capture runs `ssr:
|
|
566
|
+
route: its build-time shell capture runs `ssr: "await"` loaders and bakes their
|
|
567
567
|
settled data into the shell, like a runtime capture.
|
|
568
568
|
|
|
569
569
|
### Parallel and streaming — latency overlaps first paint
|
|
@@ -584,9 +584,9 @@ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
|
|
|
584
584
|
If you come from a framework where the loader is a blocking step that runs
|
|
585
585
|
before the response is built, this is the shift to internalize: here the
|
|
586
586
|
response starts streaming first and loader data fills in. (The one deliberate
|
|
587
|
-
exception is per-loader: `loader(Def, { ssr:
|
|
587
|
+
exception is per-loader: `loader(Def, { ssr: "await" })` awaits that
|
|
588
588
|
loader before first flush on document renders other than a `ppr` shell HIT,
|
|
589
|
-
where the shell flushes first — see "`ssr:
|
|
589
|
+
where the shell flushes first — see "`ssr: "await"`" below.)
|
|
590
590
|
|
|
591
591
|
### See it: `debugPerformance`
|
|
592
592
|
|
|
@@ -693,7 +693,7 @@ rejects) skips the whole entry, as above. Entries written
|
|
|
693
693
|
before handles were recorded replay none until they are rewritten (expiry or
|
|
694
694
|
SWR revalidation).
|
|
695
695
|
|
|
696
|
-
On a `ppr` route the shell keeps a copy of an `ssr:
|
|
696
|
+
On a `ppr` route the shell keeps a copy of an `ssr: "await"` loader's pushes
|
|
697
697
|
too, a pushed promise resolved (#1182). The two never disagree on the page:
|
|
698
698
|
where a replay of the shell serves the loader's data from the shell, the
|
|
699
699
|
shell's copy is the one shown and the entry only adds a push whose value
|
|
@@ -1005,12 +1005,12 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
1005
1005
|
|
|
1006
1006
|
Semantics by lane:
|
|
1007
1007
|
|
|
1008
|
-
| Signal | Document load, read inside `<Suspense>` or under `loading()`
|
|
1009
|
-
| ------------ |
|
|
1010
|
-
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `ssr:
|
|
1011
|
-
| `redirect()` | 200 document, then a client-side replace to the target
|
|
1008
|
+
| Signal | Document load, read inside `<Suspense>` or under `loading()` | Document load, read outside any `<Suspense>` | Client navigation |
|
|
1009
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
1010
|
+
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `ssr: "await"` (below) makes it deterministic. | A real 404; the browser renders the not-found UI with the layouts kept (the recovery document, below) | 404 UI swaps in, URL preserved, payload stays 200 |
|
|
1011
|
+
| `redirect()` | 200 document, then a client-side replace to the target | A real 3xx (the redirect's own status) with `Location`; with `state`, the recovery document (200), then the client replace carries it | Redirect envelope navigates to the target |
|
|
1012
1012
|
|
|
1013
|
-
With `loader(Def, { ssr:
|
|
1013
|
+
With `loader(Def, { ssr: "await" })` the result is settled BEFORE the document
|
|
1014
1014
|
flushes, so both signals are resolved server-side while the tree is built:
|
|
1015
1015
|
`redirect()` replaces the whole page with the redirect carrier (still a 200
|
|
1016
1016
|
document, the client replaces to the target on hydration) and `notFound()`
|
|
@@ -1102,7 +1102,7 @@ on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
|
1102
1102
|
Handle values land on their own channel and never hold the page, so an effect that needs a late push keys on the handle value, not the URL (see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design)).
|
|
1103
1103
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
1104
1104
|
the fetched data usually does not. When it MUST be in the document, use
|
|
1105
|
-
`ssr:
|
|
1105
|
+
`ssr: "await"` below. A loader with its own `cache()` keeps its pushes on a hit —
|
|
1106
1106
|
they are replayed from the entry (see "Opting a Loader into Caching"). On a
|
|
1107
1107
|
`ppr` route the race does not apply to a document served from its shell:
|
|
1108
1108
|
there every push a loader makes on that request applies post-hydration (see
|
|
@@ -1115,8 +1115,8 @@ handler reserved with `.defer()` instead (`/breadcrumbs`, "Deferred content").
|
|
|
1115
1115
|
|
|
1116
1116
|
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
1117
1117
|
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
1118
|
-
the pushes of an `ssr:
|
|
1119
|
-
shell, a pushed promise resolved, and a promise-free `ssr:
|
|
1118
|
+
the pushes of an `ssr: "await"` loader, and of the loaders it awaits, in the
|
|
1119
|
+
shell, a pushed promise resolved, and a promise-free `ssr: "await"` loader does
|
|
1120
1120
|
not run on a shell HIT — see `/ppr`.)
|
|
1121
1121
|
|
|
1122
1122
|
Reads are the other direction and gated: `ctx.get(handle)` throws unless the
|
|
@@ -1124,7 +1124,7 @@ loader first does `await ctx.rendered()` (DSL-registered loaders only —
|
|
|
1124
1124
|
handler-invoked loaders cannot use `rendered()`, and a handler already
|
|
1125
1125
|
awaiting the loader via `ctx.use()` makes it a detected deadlock).
|
|
1126
1126
|
|
|
1127
|
-
## `ssr:
|
|
1127
|
+
## `ssr: "await"` — Guarantee a Loader in the Document
|
|
1128
1128
|
|
|
1129
1129
|
Streaming means nothing a slow loader produces is _guaranteed_ in the SSR'd
|
|
1130
1130
|
HTML: its section SSRs as the fallback, a late handle push applies
|
|
@@ -1135,7 +1135,7 @@ use callback:
|
|
|
1135
1135
|
|
|
1136
1136
|
```typescript
|
|
1137
1137
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1138
|
-
loader(ProductLoader, { ssr:
|
|
1138
|
+
loader(ProductLoader, { ssr: "await" }, () => [cache({ ttl: 60 })]),
|
|
1139
1139
|
loader(RelatedLoader), // untouched: still streams behind its boundary
|
|
1140
1140
|
]),
|
|
1141
1141
|
```
|
|
@@ -1143,9 +1143,14 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
1143
1143
|
The options object is typed as `LoaderOptions`
|
|
1144
1144
|
(`import type { LoaderOptions } from "@rangojs/router"`).
|
|
1145
1145
|
|
|
1146
|
-
The
|
|
1147
|
-
|
|
1148
|
-
|
|
1146
|
+
The value says what the server render does with the loader: `"stream"` (the
|
|
1147
|
+
default, the same as leaving `ssr` out) streams it behind its boundary;
|
|
1148
|
+
`"await"` awaits it in the renders that produce HTML — the document render and
|
|
1149
|
+
a `ppr` shell capture (at runtime, or at build time on a `Prerender` + `ppr`
|
|
1150
|
+
route). (`loading(fallback, { ssr: false })` is a different option: there
|
|
1151
|
+
`false` means the fallback is not server-rendered.)
|
|
1152
|
+
|
|
1153
|
+
Document renders await this loader before first flush — data is settled (`useLoader`
|
|
1149
1154
|
reads it synchronously, no fallback paints), handle pushes beat the barrier
|
|
1150
1155
|
snapshot, and a thrown `notFound()` deterministically precedes Response
|
|
1151
1156
|
construction (real 404, no warm-up race). Client navigations stream exactly as
|
|
@@ -1212,7 +1217,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
1212
1217
|
```
|
|
1213
1218
|
|
|
1214
1219
|
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1215
|
-
`{ ssr:
|
|
1220
|
+
`{ ssr: "await", prefetch: false }` if you need both.)
|
|
1216
1221
|
|
|
1217
1222
|
| Request | unflagged | `prefetch: false` | `prefetch: "intent"` |
|
|
1218
1223
|
| ------------------------------------------------------------ | ---------- | ------------------------ | ------------------------ |
|
|
@@ -1404,7 +1409,7 @@ The limits:
|
|
|
1404
1409
|
`intercept()` defers nothing. Each intercept loader also runs its own
|
|
1405
1410
|
`revalidate()`, so an action from the modal re-runs only those that say so
|
|
1406
1411
|
(see `/intercept`).
|
|
1407
|
-
- **`ppr` and `ssr:
|
|
1412
|
+
- **`ppr` and `ssr: "await"`.** On a `ppr` route an `ssr: "await"` loader is the
|
|
1408
1413
|
bake lane. A prefetch that replays a shell a document request captured
|
|
1409
1414
|
serves it from its pin: it is not deferred, does not run, and the click
|
|
1410
1415
|
sends nothing. Every other prefetch (a navigation-only shell, a cold page,
|