@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
|
-
description: Define data loaders
|
|
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, or must be settled in the SSR'd document (ssr:false).
|
|
4
4
|
argument-hint: "[loader]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Data Loaders with loader()
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
(
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
A loader is a server function created with `createLoader()` and registered on a
|
|
10
|
+
route segment with `loader()`. It runs fresh on every request (on a `ppr` route,
|
|
11
|
+
one registered with `ssr: false` is baked into the shell instead), streams its
|
|
12
|
+
result to the client, and client components read it with `useLoader()`. This
|
|
13
|
+
skill covers defining, registering, consuming, revalidating, caching, and
|
|
14
|
+
client-fetching loaders.
|
|
15
|
+
|
|
16
|
+
For mutations (writes triggered by forms or buttons), use server actions
|
|
17
|
+
instead — see `/server-actions`. Loaders re-resolve after an action runs, so
|
|
18
|
+
the typical flow is _action mutates → loader re-reads → UI updates_.
|
|
13
19
|
|
|
14
20
|
## Not this skill if…
|
|
15
21
|
|
|
@@ -116,20 +122,37 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
116
122
|
> (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
|
|
117
123
|
> "Refreshing multiple loaders together").
|
|
118
124
|
> - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
|
|
125
|
+
> On a route `cache()` over a `ppr` route, each key value also gets its own
|
|
126
|
+
> shell; keep the value set small.
|
|
119
127
|
> - `revalidate()` — which **server** segments/loaders recompute during
|
|
120
128
|
> navigation and action refreshes.
|
|
121
129
|
|
|
122
|
-
DSL loaders are the **live data layer
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
DSL loaders are the **live data layer**: they resolve fresh on every request,
|
|
131
|
+
even when the route is inside a `cache()` boundary, so `cache()` gives you
|
|
132
|
+
cached UI + fresh data by default. See "Loaders: The Live Data Layer" below.
|
|
133
|
+
The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell,
|
|
134
|
+
and a shell HIT runs it only when its return carries promises.
|
|
127
135
|
|
|
128
136
|
### Cache safety
|
|
129
137
|
|
|
130
|
-
DSL
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
A DSL loader without `ssr: false` can read `createVar({ cache: false })`
|
|
139
|
+
variables: it runs on every request, `cache()` and PPR shell HITs included, so
|
|
140
|
+
the read guard is bypassed for it. On a `ppr` route the capture runs an
|
|
141
|
+
`ssr: false` loader, and a loader a handler awaits, and bakes their values into
|
|
142
|
+
the shell: a `cookies()`, `headers()` or `{ cache: false }` read there refuses
|
|
143
|
+
the capture, so the route keeps rendering without a shell (warned once per key).
|
|
144
|
+
A loader bound with its own `cache()` is the other exception: its value is
|
|
145
|
+
stored and shared, so a miss whose body read `cookies()`, `headers()`, a
|
|
146
|
+
non-cacheable variable or the theme fails unless the binding has a `key()` or a
|
|
147
|
+
store `keyGenerator`, which must then include them (see "Cache Key"). A
|
|
148
|
+
`"use cache"` function the loader calls is not a loader body: a non-cacheable
|
|
149
|
+
read inside it throws, also through the loader's `ctx` passed in, so read the
|
|
150
|
+
var in the loader and pass the value in as an argument. Passing the loader's
|
|
151
|
+
`ctx` itself is fine: the call keys by route, params and search, like a handler
|
|
152
|
+
`ctx` (`/use-cache`). A fetchable loader called with a request body
|
|
153
|
+
(`load({ method: "POST", body })` or form data) is the exception: the body is
|
|
154
|
+
not in the key, so the call runs uncached. Pass the body fields the function
|
|
155
|
+
needs as arguments instead.
|
|
133
156
|
|
|
134
157
|
### ctx.use(Loader) — escape hatch
|
|
135
158
|
|
|
@@ -137,6 +160,7 @@ For cases where you need loader data in the server handler itself (e.g.,
|
|
|
137
160
|
to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
|
|
138
161
|
|
|
139
162
|
```typescript
|
|
163
|
+
// Product is a context-variable token: export const Product = createVar<ProductData>();
|
|
140
164
|
path("/product/:slug", async (ctx) => {
|
|
141
165
|
const { product } = await ctx.use(ProductLoader);
|
|
142
166
|
ctx.set(Product, product); // make available to children
|
|
@@ -146,19 +170,31 @@ path("/product/:slug", async (ctx) => {
|
|
|
146
170
|
])
|
|
147
171
|
```
|
|
148
172
|
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
`ctx.use(Loader)` is a per-request read on any render that runs the handler,
|
|
174
|
+
memoized for the request (a route `cache()` HIT or a `ppr` shell HIT runs no
|
|
175
|
+
handler, so the handler's output from the read is replayed): however many
|
|
176
|
+
times it is called, and whether or not the loader is also registered with
|
|
177
|
+
`loader()`, it runs once per request. It never reads or writes the loader
|
|
178
|
+
store cache on its own: a loader's `cache()` belongs to its DSL binding
|
|
179
|
+
(`loader(Def, () => [cache()])` on a route, layout or intercept). A handler
|
|
180
|
+
read of a cached binding gets the binding's (possibly cached) result once the
|
|
181
|
+
binding has started (a read from its own entry's handler or from later in
|
|
182
|
+
the tree).
|
|
151
183
|
|
|
152
184
|
**Limitations of ctx.use(Loader):**
|
|
153
185
|
|
|
154
186
|
- The handler output depends on the loader data. If the route is inside
|
|
155
187
|
`cache()`, the handler is cached with the loader result baked in —
|
|
156
188
|
defeating the live data guarantee.
|
|
157
|
-
- The same holds under a PPR shell capture (`/ppr`): handler consumption is
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
- The same holds under a PPR shell capture (`/ppr`): handler consumption is the
|
|
190
|
+
BAKED lane — the loader executes at capture and the rendered value is a
|
|
191
|
+
capture-time copy served on every HIT (a HIT never runs the handler);
|
|
192
|
+
`useLoader` of a loader without `ssr: false` is the live lane (an `ssr: false`
|
|
193
|
+
loader is the bake lane however it is read). Unlike `cache()`, an identity
|
|
194
|
+
read inside that loader (`cookies()`, `headers()`, a `{ cache: false }`
|
|
195
|
+
variable) refuses the capture, so the route stays uncached. One rule across
|
|
196
|
+
`cache()`, `"use cache"`, and PPR: the consumption-lane rule (`/rango` →
|
|
197
|
+
Invariants).
|
|
162
198
|
- Non-cacheable variable reads (`createVar({ cache: false })`) inside the
|
|
163
199
|
handler still throw, even if the data came from a loader.
|
|
164
200
|
- Prefer DSL `loader()` + client `useLoader()` for data that depends on
|
|
@@ -175,27 +211,34 @@ same memoized result — loaders never run twice per request.
|
|
|
175
211
|
|
|
176
212
|
## Loader Context
|
|
177
213
|
|
|
178
|
-
Loaders receive
|
|
214
|
+
Loaders receive a request-scoped context that shares its read surface with
|
|
215
|
+
route handlers (`params`, `request`, `url`, `env`, `get`, `use`, `reverse`).
|
|
216
|
+
It has no handler-only writers: there is no `ctx.set`, `ctx.headers`, or
|
|
217
|
+
`ctx.setLocationState` (a loader can stream after the response has started,
|
|
218
|
+
so response-shaping belongs in middleware or the handler).
|
|
179
219
|
|
|
180
220
|
### Full field surface
|
|
181
221
|
|
|
182
|
-
| Field
|
|
183
|
-
|
|
|
184
|
-
| `params`
|
|
185
|
-
| `routeParams`
|
|
186
|
-
| `request`
|
|
187
|
-
| `url`
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
222
|
+
| Field | Type | Notes |
|
|
223
|
+
| ------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
225
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
226
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
227
|
+
| `url` | `URL` | Request URL with internal `_rsc*` transport params stripped. Use this for application logic. |
|
|
228
|
+
| `originalUrl` | `URL` | Request URL with every param intact, including `_rsc*`. Only for debugging or custom cache keying. |
|
|
229
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
230
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
231
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
232
|
+
| `env` | `any` | Bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). `createLoader` types it as `any`; annotate or cast at the use site. |
|
|
233
|
+
| `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. |
|
|
234
|
+
| `executionContext` | `ExecutionContext \| undefined` | Raw Cloudflare `ExecutionContext` for libraries that need it; `undefined` off Cloudflare. Prefer `waitUntil`. |
|
|
235
|
+
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`. |
|
|
236
|
+
| `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`. |
|
|
237
|
+
| `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: false` (cycle; throws). |
|
|
238
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
239
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
240
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
241
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
199
242
|
|
|
200
243
|
### Example
|
|
201
244
|
|
|
@@ -275,10 +318,13 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
275
318
|
revalidate(() => false), // Never revalidate
|
|
276
319
|
]),
|
|
277
320
|
|
|
278
|
-
// Loader that
|
|
279
|
-
//
|
|
321
|
+
// Loader that re-runs only after cart actions. Loaders re-run after every
|
|
322
|
+
// action by default; on navigation, `undefined` keeps the default
|
|
323
|
+
// (re-run when params or search change).
|
|
280
324
|
loader(CartLoader, () => [
|
|
281
|
-
revalidate((ctx) =>
|
|
325
|
+
revalidate((ctx) =>
|
|
326
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
327
|
+
),
|
|
282
328
|
]),
|
|
283
329
|
]);
|
|
284
330
|
```
|
|
@@ -292,7 +338,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
292
338
|
> Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
|
|
293
339
|
> See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
294
340
|
|
|
295
|
-
A `revalidate(fn)` callback
|
|
341
|
+
A `revalidate(fn)` callback returns one of three kinds of value. The chain
|
|
296
342
|
processes revalidators in order; each call's return controls how the
|
|
297
343
|
chain continues:
|
|
298
344
|
|
|
@@ -318,8 +364,24 @@ revalidate(() => undefined); // explicit defer
|
|
|
318
364
|
revalidate(() => null); // explicit defer
|
|
319
365
|
```
|
|
320
366
|
|
|
321
|
-
If every revalidator on a segment defers, the segment
|
|
322
|
-
(
|
|
367
|
+
If every revalidator on a segment defers, the segment default is used
|
|
368
|
+
(`defaultShouldRevalidate`, see the table below).
|
|
369
|
+
|
|
370
|
+
The callback must be **synchronous**. A returned Promise is ignored (dev
|
|
371
|
+
warning) and the running suggestion is kept, so move async work into a loader.
|
|
372
|
+
A callback that throws is logged and treated as a defer; a thrown `Response`
|
|
373
|
+
(e.g. `throw redirect(...)`) propagates as control flow.
|
|
374
|
+
|
|
375
|
+
Segment defaults when no revalidator decides (from
|
|
376
|
+
`src/router/revalidation.ts`). A segment the client does not hold yet always
|
|
377
|
+
renders.
|
|
378
|
+
|
|
379
|
+
| Segment | After an action (POST) | Navigation |
|
|
380
|
+
| -------------------------------------------------------------------- | ---------------------- | ----------------------------------- |
|
|
381
|
+
| Route (`path()`) | `true` | `true` when params or search change |
|
|
382
|
+
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true` | `true` when params or search change |
|
|
383
|
+
| Loader above the route (e.g. on a parent `layout()`) | `true` | `false` |
|
|
384
|
+
| Layout or parallel above the route | `false` | `false` |
|
|
323
385
|
|
|
324
386
|
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
325
387
|
|
|
@@ -335,17 +397,28 @@ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
|
|
|
335
397
|
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
336
398
|
```
|
|
337
399
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
400
|
+
Pick the idiom by what the segment's default already does (table above). After
|
|
401
|
+
an action, loaders, the route, and segments inside the `path()` already re-run;
|
|
402
|
+
layouts and parallels above the route are skipped:
|
|
403
|
+
|
|
404
|
+
- **Add a signal where the default skips.** `ctx.isAction(X) || undefined` on a
|
|
405
|
+
parent layout or a layout-level parallel re-renders it after `X` and defers
|
|
406
|
+
otherwise. On a loader, the route, or a segment inside the `path()` it
|
|
407
|
+
changes nothing: they already re-run after every action, and on navigation it
|
|
408
|
+
defers.
|
|
409
|
+
- **Narrow after actions where the default re-runs.**
|
|
410
|
+
`ctx.isAction() ? ctx.isAction(X) : undefined` re-runs a loader or route
|
|
411
|
+
segment only for `X`'s actions and keeps the navigation default (params or
|
|
412
|
+
search changed). After an action it is a hard decision, so later revalidators
|
|
413
|
+
on the same segment do not run for actions.
|
|
414
|
+
- **Avoid bare `ctx.isAction(X)` (and `?? false`).** It returns a hard `false`
|
|
415
|
+
on navigation too, so a loader stops refetching when you navigate to a
|
|
416
|
+
different `:id`. Reserve it for the rare case where the segment should re-run
|
|
417
|
+
on nothing but the matched action.
|
|
418
|
+
|
|
419
|
+
When **composing multiple revalidators** on one segment (see below), prefer the
|
|
420
|
+
defer form: the first hard `false` ends the chain and the later contracts never
|
|
421
|
+
run.
|
|
349
422
|
|
|
350
423
|
#### Matching actions: `ctx.isAction()`
|
|
351
424
|
|
|
@@ -358,21 +431,31 @@ import { addToCart, removeFromCart } from "../actions/cart";
|
|
|
358
431
|
import * as CartActions from "../actions/cart";
|
|
359
432
|
|
|
360
433
|
loader(CartLoader, () => [
|
|
361
|
-
|
|
434
|
+
// After an action, re-run only for addToCart; on navigation, keep the default.
|
|
435
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(addToCart) : undefined)),
|
|
362
436
|
]);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
437
|
+
|
|
438
|
+
// Matcher forms (each returns a boolean):
|
|
439
|
+
ctx.isAction(addToCart); // one action
|
|
440
|
+
ctx.isAction(addToCart, removeFromCart); // several
|
|
441
|
+
ctx.isAction(CartActions); // any action in the module
|
|
442
|
+
ctx.isAction({ addToCart, removeFromCart }); // object form
|
|
443
|
+
ctx.isAction({ Cart: CartActions }); // grouped namespaces
|
|
444
|
+
ctx.isAction(); // no args: any action at all
|
|
366
445
|
```
|
|
367
446
|
|
|
368
447
|
`isAction()` is a method on the revalidate predicate's **context argument** —
|
|
369
448
|
there is no standalone `isAction` import; you always reach it through the callback
|
|
370
449
|
parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
450
|
+
wrap it by placement (the three cases above): the ternary on a loader or route
|
|
451
|
+
segment, `|| undefined` on a layout or layout-level parallel the default skips.
|
|
452
|
+
It returns `false` on plain navigation and on non-matches; called with no
|
|
453
|
+
arguments it answers "is this request an action?". It resolves the reference with
|
|
454
|
+
the same `$id ?? $$id` precedence the router uses to derive `actionId`, so it
|
|
455
|
+
matches in both dev and production. The raw `actionId` string stays available on
|
|
456
|
+
the same context as an escape hatch, alongside `actionResult`, `formData`,
|
|
457
|
+
`actionUrl`, `currentParams`/`nextParams`, `currentUrl`/`nextUrl`,
|
|
458
|
+
`fromRouteName`/`toRouteName`, and the full handler `context`.
|
|
376
459
|
|
|
377
460
|
### Revalidation Contracts for Loader Dependencies
|
|
378
461
|
|
|
@@ -381,18 +464,23 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
381
464
|
|
|
382
465
|
```typescript
|
|
383
466
|
// revalidation-contracts.ts
|
|
467
|
+
import type { Revalidate } from "@rangojs/router";
|
|
384
468
|
import * as AccountActions from "./actions/account";
|
|
385
469
|
|
|
386
470
|
// Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
|
|
387
471
|
// so these contracts compose — a hard `false` would short-circuit the rest.
|
|
388
|
-
export const revalidateAccountScope = (ctx) =>
|
|
472
|
+
export const revalidateAccountScope: Revalidate = (ctx) =>
|
|
389
473
|
ctx.isAction(AccountActions) || undefined;
|
|
390
474
|
|
|
475
|
+
// urls.tsx
|
|
391
476
|
layout(AccountLayout, () => [
|
|
392
|
-
|
|
477
|
+
// producer: adds the re-render the default skips for a parent layout
|
|
478
|
+
revalidate(revalidateAccountScope),
|
|
393
479
|
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
394
480
|
loader(OrdersLoader, () => [
|
|
395
|
-
|
|
481
|
+
// consumer: loaders re-run after every action anyway; the shared
|
|
482
|
+
// contract names the dependency
|
|
483
|
+
revalidate(revalidateAccountScope),
|
|
396
484
|
]),
|
|
397
485
|
]),
|
|
398
486
|
]);
|
|
@@ -418,16 +506,20 @@ layout(AccountLayout, () => [
|
|
|
418
506
|
|
|
419
507
|
## Loaders: The Live Data Layer
|
|
420
508
|
|
|
421
|
-
Loaders
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
the
|
|
509
|
+
Loaders resolve fresh on every request, even when the route's UI segments are
|
|
510
|
+
served from cache. Route-level `cache()` caches rendered segments but never
|
|
511
|
+
loader data: loaders are excluded when the segments are stored and re-resolved
|
|
512
|
+
when they are served. Caching a loader's own data is a separate opt-in (see
|
|
513
|
+
"Opting a Loader into Caching"). A `ppr` shell does store some loader data: an
|
|
514
|
+
`ssr: false` loader's settled data bakes into the shell, and a HIT runs that
|
|
515
|
+
loader only when its return carries promises (see "`ssr: false`" below and
|
|
516
|
+
`/ppr`).
|
|
517
|
+
|
|
518
|
+
Pre-rendering follows the same rule: the build-time Flight payload skips
|
|
519
|
+
loaders (there is no real request context), and at runtime the worker resolves
|
|
520
|
+
them fresh against the live database. The exception is a `Prerender` + `ppr`
|
|
521
|
+
route: its build-time shell capture runs `ssr: false` loaders and bakes their
|
|
522
|
+
settled data into the shell, like a runtime capture.
|
|
431
523
|
|
|
432
524
|
### Parallel and streaming — latency overlaps first paint
|
|
433
525
|
|
|
@@ -448,8 +540,8 @@ If you come from a framework where the loader is a blocking step that runs
|
|
|
448
540
|
before the response is built, this is the shift to internalize: here the
|
|
449
541
|
response starts streaming first and loader data fills in. (The one deliberate
|
|
450
542
|
exception is per-loader: `loader(Def, { ssr: false })` awaits that
|
|
451
|
-
loader before first flush on document renders
|
|
452
|
-
below.)
|
|
543
|
+
loader before first flush on document renders other than a `ppr` shell HIT,
|
|
544
|
+
where the shell flushes first — see "`ssr: false`" below.)
|
|
453
545
|
|
|
454
546
|
### See it: `debugPerformance`
|
|
455
547
|
|
|
@@ -462,16 +554,26 @@ const router = createRouter({ document: Document, debugPerformance: true });
|
|
|
462
554
|
```
|
|
463
555
|
|
|
464
556
|
Or enable it per-request from middleware (e.g. only when `?debug` is present) by
|
|
465
|
-
calling `ctx.debugPerformance()` **before** `await next()
|
|
466
|
-
|
|
557
|
+
calling `ctx.debugPerformance()` **before** `await next()`:
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
router.use(async (ctx, next) => {
|
|
561
|
+
if (ctx.searchParams.has("debug")) ctx.debugPerformance();
|
|
562
|
+
return next();
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Each metered request then prints a shared-axis waterfall to the server console
|
|
567
|
+
(and adds the timings to the `Server-Timing` response header):
|
|
467
568
|
|
|
468
569
|
```
|
|
469
570
|
[RSC Perf] GET /product/widget (24.53ms)
|
|
470
571
|
start dur span timeline
|
|
471
572
|
0.08ms 3.20ms route-matching |#####...................................|
|
|
472
|
-
3.
|
|
573
|
+
3.30ms 20.10ms render:total:product |....############################........|
|
|
574
|
+
3.40ms 8.70ms ssr:render-html |.....##############.....................|
|
|
473
575
|
3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
|
|
474
|
-
3.45ms 11.40ms loader:…#ReviewsLoader
|
|
576
|
+
3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
|
|
475
577
|
0.00ms 24.53ms handler:total |########################################|
|
|
476
578
|
```
|
|
477
579
|
|
|
@@ -479,12 +581,13 @@ How to read it:
|
|
|
479
581
|
|
|
480
582
|
- **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
|
|
481
583
|
offset and run side by side are executing **in parallel** — loaders should
|
|
482
|
-
overlap `ssr
|
|
584
|
+
overlap `ssr:render-html` / `render:total`, not sit alone to the right of
|
|
483
585
|
everything. A lone `loader:*` bar past the render bar is serialized latency to
|
|
484
|
-
chase. `handler:total` is the whole request; `render:total` is the render pass
|
|
586
|
+
chase. `handler:total` is the whole request; `render:total` is the render pass
|
|
587
|
+
(labelled `render:total:<routeName>` when the matched route has a name).
|
|
485
588
|
- **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
|
|
486
589
|
overlaps paint when its `[start, start+dur]` interval intersects
|
|
487
|
-
`render:total` / `ssr
|
|
590
|
+
`render:total` / `ssr:render-html`. Flag a regression when a `loader:*`
|
|
488
591
|
interval is **disjoint from and starts after** `render:total`, or when its
|
|
489
592
|
`dur` approaches `handler:total` — that loader is on the critical path instead
|
|
490
593
|
of overlapping it. Two `loader:*` rows with near-equal `start` confirm
|
|
@@ -498,6 +601,14 @@ To cache a specific loader's data, attach a `cache()` child:
|
|
|
498
601
|
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
499
602
|
```
|
|
500
603
|
|
|
604
|
+
This works wherever the DSL binding is declared, an intercept's `use()`
|
|
605
|
+
included. A handler's `ctx.use(Loader)` read never caches by itself (see
|
|
606
|
+
"ctx.use(Loader) — escape hatch"). Once the binding started, every
|
|
607
|
+
`ctx.use(Loader)` in the request, from the handler or from another loader's
|
|
608
|
+
body, gets the binding's value: one value per loader per request, a HIT
|
|
609
|
+
included. A reader that runs before the binding starts (a layout's loader
|
|
610
|
+
reading a route's cached loader) gets the value of its own run, as before.
|
|
611
|
+
|
|
501
612
|
The loader's data is cached independently from the route's segment cache,
|
|
502
613
|
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
503
614
|
|
|
@@ -505,12 +616,43 @@ Values are serialized through RSC Flight, so loaders can return ReactNode,
|
|
|
505
616
|
Promises, null, and any RSC-serializable type — all round-trip correctly
|
|
506
617
|
through the cache.
|
|
507
618
|
|
|
619
|
+
A value Flight cannot encode cleanly is not cached: a promise in it that
|
|
620
|
+
rejects, an async component that throws, a function or a class instance.
|
|
621
|
+
Storing it would bake the failure into every hit until expiry. Instead the
|
|
622
|
+
write is skipped and reported to `onError` (phase `"cache"`, category
|
|
623
|
+
`cache-write`, or `stale-revalidation` for a background refresh): a miss still
|
|
624
|
+
returns the loader's live result, the next request runs the loader again, and
|
|
625
|
+
a stale entry keeps serving.
|
|
626
|
+
|
|
627
|
+
Handle pushes the body makes (`ctx.use(Meta)(...)`, breadcrumbs — see
|
|
628
|
+
"Writing Handles from Loaders"), including those of loaders it awaits via
|
|
629
|
+
`ctx.use`, are stored with the value and replayed on every hit, stale hits
|
|
630
|
+
included: appended to the loader's segment in push order, so a cached
|
|
631
|
+
loader's title or crumb stays on the page. Relative to the handler's and
|
|
632
|
+
sibling loaders' pushes, the replay lands when the cache read completes —
|
|
633
|
+
the same race a live loader push has. A loader the cached body awaited can
|
|
634
|
+
still run on a hit, because a sibling loader or the handler also reads it
|
|
635
|
+
(loaders stay live); its pushes still appear once, and they are the live
|
|
636
|
+
ones. If it ran before the replay, the replay skips its recorded pushes; if
|
|
637
|
+
it runs after, its pushes replace the replayed ones in their position (after
|
|
638
|
+
hydration, when it lands after the document's handle snapshot). A pending push
|
|
639
|
+
(a promise, an async callback, a `.defer()` slot) is recorded once it settles,
|
|
640
|
+
as in `"use cache"`: the background write waits up to 5 s, and a push still
|
|
641
|
+
pending at 5 s drops that entry's handle record (the data is still cached; its
|
|
642
|
+
hits replay no pushes). A pushed value Flight cannot encode (a promise that
|
|
643
|
+
rejects) skips the whole entry, as above. Entries written
|
|
644
|
+
before handles were recorded replay none until they are rewritten (expiry or
|
|
645
|
+
SWR revalidation).
|
|
646
|
+
|
|
508
647
|
### Cache Key
|
|
509
648
|
|
|
510
|
-
The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}
|
|
511
|
-
This can be customized
|
|
649
|
+
The default cache key is `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
650
|
+
(the host keeps multi-tenant hosts from sharing entries). This can be customized
|
|
651
|
+
at two levels:
|
|
512
652
|
|
|
513
653
|
```typescript
|
|
654
|
+
import { cookies } from "@rangojs/router";
|
|
655
|
+
|
|
514
656
|
// Full override — key function replaces the default entirely
|
|
515
657
|
loader(ProductLoader, () => [
|
|
516
658
|
cache({
|
|
@@ -527,10 +669,62 @@ Resolution priority (same as route-level `cache()`):
|
|
|
527
669
|
|
|
528
670
|
1. `key(ctx)` from cache options — full override
|
|
529
671
|
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
|
|
530
|
-
3. Default key — `loader:{
|
|
672
|
+
3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
673
|
+
|
|
674
|
+
A `key` function (or store `keyGenerator`) that throws is **not** caught: the
|
|
675
|
+
loader fails as if its body threw. There is no silent fallback to the default
|
|
676
|
+
key, because a personalised key collapsing onto the broad default would share
|
|
677
|
+
one user's data with everyone.
|
|
678
|
+
|
|
679
|
+
**Request-scoped reads in the body must be part of the key.** The default key
|
|
680
|
+
names no user, so one entry serves everyone who requests the same loader, host,
|
|
681
|
+
path and params. A route `cache()` around the route does not partition it
|
|
682
|
+
either: a loader's own `cache()` is an independent layer, keyed only by what it
|
|
683
|
+
declares. On a miss, a body that reads `cookies()`, `headers()` or a
|
|
684
|
+
non-cacheable variable (`createVar({ cache: false })`, or a value written with
|
|
685
|
+
`ctx.set(..., { cache: false })`) therefore **fails**, and nothing is stored,
|
|
686
|
+
unless the binding declares identity with a `key()` or a store `keyGenerator`.
|
|
687
|
+
The check tests only that one of them is there, not what it contains: any
|
|
688
|
+
`key()` or store `keyGenerator` switches it off, so it must itself include the
|
|
689
|
+
value. That includes a store-wide `keyGenerator` that only adds a region
|
|
690
|
+
prefix, like the one described above.
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
export const SessionLoader = createLoader(async () =>
|
|
694
|
+
getAccount(cookies().get("session")?.value),
|
|
695
|
+
);
|
|
531
696
|
|
|
532
|
-
|
|
533
|
-
(
|
|
697
|
+
// The miss fails: cookies() cannot be called inside loader "…SessionLoader",
|
|
698
|
+
// whose own cache() has no key().
|
|
699
|
+
loader(SessionLoader, () => [cache({ ttl: 60 })]),
|
|
700
|
+
|
|
701
|
+
// One entry per session.
|
|
702
|
+
loader(SessionLoader, () => [
|
|
703
|
+
cache({
|
|
704
|
+
ttl: 60,
|
|
705
|
+
key: (ctx) => `account:${cookies().get("session")?.value ?? "anon"}`,
|
|
706
|
+
}),
|
|
707
|
+
]),
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
The same holds in a stale entry's background refresh (the refresh fails and the
|
|
711
|
+
stale entry keeps serving) and for the loaders the body reads with `ctx.use()`,
|
|
712
|
+
because their values land in the entry. That includes a keyed cached loader
|
|
713
|
+
served from its own cache: its entry keeps the read its miss made. It also holds
|
|
714
|
+
when something else ran the loader first, such as a parent layout's handler
|
|
715
|
+
calling `ctx.use(SessionLoader)`: the miss reuses that run and fails the same
|
|
716
|
+
way. A read that settles after the value, in a promise inside it or in a
|
|
717
|
+
handle push that settles within the 5 s handle-encode timeout, can't fail a
|
|
718
|
+
value that was already served: that user gets their own value, nothing is
|
|
719
|
+
stored, and `onError` gets the same error. A push still pending after the
|
|
720
|
+
timeout drops the entry's handles, and the value is stored without them. A loader with no `cache()` of its own
|
|
721
|
+
reads request data freely.
|
|
722
|
+
|
|
723
|
+
Two kinds of read are not seen, the same as in `"use cache"`: raw reads such
|
|
724
|
+
as `ctx.request.headers` or `getRequestContext().cookie()`, and a value
|
|
725
|
+
computed outside the loader and handed in, such as a per-request memo a handler
|
|
726
|
+
filled from `cookies()` before the loader awaited it. Key what those carry
|
|
727
|
+
too. If the value must stay per request, drop the loader's `cache()` instead.
|
|
534
728
|
|
|
535
729
|
### Tags for Invalidation
|
|
536
730
|
|
|
@@ -549,6 +743,34 @@ loader(ProductLoader, () => [
|
|
|
549
743
|
]),
|
|
550
744
|
```
|
|
551
745
|
|
|
746
|
+
The tags the loader body records tag the entry too: `cacheTag()` calls, the
|
|
747
|
+
tags of `"use cache"` reads, render-time `cacheTag()` in server components of
|
|
748
|
+
the value or a handle push, and the tags the loaders it reads with `ctx.use`
|
|
749
|
+
record, including one the route, a layout or the handler started first. A
|
|
750
|
+
loader it reads that has its own `cache()` also brings its `cache({ tags })`
|
|
751
|
+
and, on a HIT, its entry's tags. `updateTag()` of any of them drops the entry,
|
|
752
|
+
and a HIT records them again, so a route `cache()`, ppr shell or document built
|
|
753
|
+
over the HIT stays evictable. A stale hit's background refresh stores the
|
|
754
|
+
refreshed body's tags; it runs the loaders it reads again rather than reusing
|
|
755
|
+
the page's (possibly stale) values.
|
|
756
|
+
|
|
757
|
+
One case keeps a `cache({ tags })` off: a cached loader that reads another
|
|
758
|
+
loader's `cache()` binding before that binding starts (for example a cached
|
|
759
|
+
layout loader reading a route's cached loader), and writes its entry before
|
|
760
|
+
it. That reader's entry has the tags the loader's body records, but not the
|
|
761
|
+
tags only its `cache({ tags })` lists. If the reader must drop with them, record
|
|
762
|
+
them in the loader body with `cacheTag()` too.
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
766
|
+
cacheTag(`product:${ctx.params.slug}`);
|
|
767
|
+
return db.product(ctx.params.slug);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
loader(ProductLoader, () => [cache({ ttl: 600 })]),
|
|
771
|
+
// updateTag(`product:${slug}`) drops this entry.
|
|
772
|
+
```
|
|
773
|
+
|
|
552
774
|
### Stale-While-Revalidate
|
|
553
775
|
|
|
554
776
|
```typescript
|
|
@@ -589,8 +811,9 @@ loader(PricingLoader, () => [
|
|
|
589
811
|
]),
|
|
590
812
|
```
|
|
591
813
|
|
|
592
|
-
Without an explicit store, the loader uses the app-level store from the
|
|
593
|
-
|
|
814
|
+
Without an explicit store, the loader uses the app-level store from the router
|
|
815
|
+
config (`createRouter({ cache: { store } })`). Without any store, `cache()` on
|
|
816
|
+
a loader is inert and the loader runs fresh.
|
|
594
817
|
|
|
595
818
|
## Multiple Loaders
|
|
596
819
|
|
|
@@ -621,8 +844,9 @@ layout(<ShopLayout />, () => [
|
|
|
621
844
|
|
|
622
845
|
## Passing Loaders as Props
|
|
623
846
|
|
|
624
|
-
Loaders can be passed as props from server to client components.
|
|
625
|
-
|
|
847
|
+
Loaders can be passed as props from server to client components. A loader
|
|
848
|
+
definition is a plain `{ __brand, $$id }` object (the function itself stays in the
|
|
849
|
+
server registry), so it serializes as-is.
|
|
626
850
|
|
|
627
851
|
```typescript
|
|
628
852
|
// Server component (route handler)
|
|
@@ -700,9 +924,10 @@ flushes, so both signals are resolved server-side while the tree is built:
|
|
|
700
924
|
`redirect()` replaces the whole page with the redirect carrier (still a 200
|
|
701
925
|
document, the client replaces to the target on hydration) and `notFound()`
|
|
702
926
|
renders the not-found UI at the owning segment with a real 404. No read site
|
|
703
|
-
runs, so a `useLoader` in a layout above every Suspense boundary is safe.
|
|
704
|
-
|
|
705
|
-
|
|
927
|
+
runs, so a `useLoader` in a layout above every Suspense boundary is safe. On a
|
|
928
|
+
`ppr` route, a flagged loader that settles with `notFound()` or `redirect()`
|
|
929
|
+
during the shell capture refuses the capture (warned once per key): that URL
|
|
930
|
+
gets no shell, and its renders deliver the signal as above.
|
|
706
931
|
|
|
707
932
|
Session/auth gates belong in middleware (they are request-shaped, not
|
|
708
933
|
data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
|
|
@@ -731,12 +956,19 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
731
956
|
```
|
|
732
957
|
|
|
733
958
|
Delivery is async **by the race model**: pushes that settle before the handler
|
|
734
|
-
barrier ride the SSR handle snapshot (in the SSR'd document — `<
|
|
959
|
+
barrier ride the SSR handle snapshot (in the SSR'd document — `<Html.Meta />`,
|
|
735
960
|
`useHandle` reads); later pushes stream to the client and apply post-hydration
|
|
736
961
|
on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
737
962
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
738
963
|
the fetched data usually does not. When it MUST be in the document, use
|
|
739
|
-
`ssr: false` below.
|
|
964
|
+
`ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
|
|
965
|
+
they are replayed from the entry (see "Opting a Loader into Caching").
|
|
966
|
+
|
|
967
|
+
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
968
|
+
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
969
|
+
the settled pushes of an `ssr: false` loader, and of the loaders it awaits, in
|
|
970
|
+
the shell, and a promise-free `ssr: false` loader does not run on a shell HIT —
|
|
971
|
+
see `/ppr`.)
|
|
740
972
|
|
|
741
973
|
Reads are the other direction and gated: `ctx.get(handle)` throws unless the
|
|
742
974
|
loader first does `await ctx.rendered()` (DSL-registered loaders only —
|
|
@@ -759,14 +991,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
759
991
|
]),
|
|
760
992
|
```
|
|
761
993
|
|
|
762
|
-
The
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
994
|
+
The options object is typed as `LoaderOptions`
|
|
995
|
+
(`import type { LoaderOptions } from "@rangojs/router"`).
|
|
996
|
+
|
|
997
|
+
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off for
|
|
998
|
+
this loader, so nothing of it is left to stream in the document: document
|
|
999
|
+
renders await this loader before first flush — data is settled (`useLoader`
|
|
1000
|
+
reads it synchronously, no fallback paints), handle pushes beat the barrier
|
|
1001
|
+
snapshot, and a thrown `notFound()` deterministically precedes Response
|
|
1002
|
+
construction (real 404, no warm-up race). Client navigations stream exactly as
|
|
1003
|
+
before (on a `ppr` route, a navigation that replays a shell captured by a
|
|
1004
|
+
document request serves a promise-free flagged loader from its pin, as a
|
|
1005
|
+
document HIT does; a snapshot captured by a navigation alone carries no pins,
|
|
1006
|
+
and its replay runs the loader fresh). Scoped per LOADER: the flagged loader
|
|
1007
|
+
awaits only itself; siblings keep streaming.
|
|
770
1008
|
|
|
771
1009
|
Delivery is in-place, not merely in-document. React's Fizz outlines any
|
|
772
1010
|
COMPLETED Suspense boundary over ~500 bytes to an end-of-stream
|
|
@@ -782,28 +1020,35 @@ still outline; their reveal must wait for the CSS.
|
|
|
782
1020
|
|
|
783
1021
|
The costs and constraints:
|
|
784
1022
|
|
|
785
|
-
- Every document
|
|
786
|
-
That is the point — but keep flagged
|
|
787
|
-
routes.
|
|
1023
|
+
- Every document render that is not a `ppr` shell HIT pays the flagged
|
|
1024
|
+
loader's latency before first byte. That is the point — but keep flagged
|
|
1025
|
+
loaders fast, and flag loaders, not routes. On a HIT the shell flushes
|
|
1026
|
+
first: a flagged loader whose return has no promises does not run (unless
|
|
1027
|
+
the capture marked it to run: see `/ppr` → On a shell HIT), and one whose
|
|
1028
|
+
return carries promises runs to fill its holes.
|
|
788
1029
|
- A flagged loader must not `await ctx.rendered()` / `ctx.get(handle)` — the
|
|
789
1030
|
document render awaits the loader before the render barrier resolves, so
|
|
790
1031
|
that wait is a cycle by construction; it throws a deadlock error naming the
|
|
791
1032
|
fix.
|
|
792
|
-
- PPR
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1033
|
+
- Under PPR the flag is also the bake lane: the settled non-promise data
|
|
1034
|
+
(handle pushes included) freezes into the stored shell — see `/ppr` → The
|
|
1035
|
+
loader lane rule. On a HIT, a flagged loader whose return has no promises is
|
|
1036
|
+
served from the shell and does not run (unless the capture marked it to run:
|
|
1037
|
+
see `/ppr` → On a shell HIT); one whose return carries promises
|
|
1038
|
+
runs on every HIT, with its baked parts overlaid. A `cookies()`, `headers()`
|
|
1039
|
+
or `{ cache: false }` read in it refuses the capture. The
|
|
1040
|
+
`progressiveChunkSize` auto-raise is live-document only; captured shells
|
|
1041
|
+
outline per the explicit option or React's default.
|
|
797
1042
|
|
|
798
1043
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
799
|
-
loader-heavy shape makes it most useful.
|
|
800
|
-
package root.
|
|
1044
|
+
loader-heavy shape makes it most useful.
|
|
801
1045
|
|
|
802
1046
|
## Fetchable Loaders
|
|
803
1047
|
|
|
804
|
-
By default, loaders only run
|
|
805
|
-
|
|
806
|
-
|
|
1048
|
+
By default, loaders only run as part of a render (document, navigation, or
|
|
1049
|
+
post-action revalidation); the `_rsc_loader` endpoint rejects them. Pass `true`
|
|
1050
|
+
as the second argument to `createLoader` to make a loader **fetchable** —
|
|
1051
|
+
callable from the client via `useFetchLoader()` and `load()`:
|
|
807
1052
|
|
|
808
1053
|
```typescript
|
|
809
1054
|
import { createLoader } from "@rangojs/router";
|
|
@@ -839,9 +1084,10 @@ requireAuth] })` (next section).
|
|
|
839
1084
|
|
|
840
1085
|
### Fetchable Loader with Middleware
|
|
841
1086
|
|
|
842
|
-
Pass an options object instead of `true` to attach per-loader middleware
|
|
843
|
-
This middleware runs only on
|
|
844
|
-
`
|
|
1087
|
+
Pass an options object instead of `true` to attach per-loader middleware (any
|
|
1088
|
+
options object makes the loader fetchable). This middleware runs only on
|
|
1089
|
+
`_rsc_loader` fetch requests (client-side `load()` / `useFetchLoader()` /
|
|
1090
|
+
`useRefreshLoaders()` calls), not when the loader runs as part of a render:
|
|
845
1091
|
|
|
846
1092
|
```typescript
|
|
847
1093
|
import { createLoader } from "@rangojs/router";
|
|
@@ -893,6 +1139,13 @@ export const MutationLoader = createLoader(async (ctx) => {
|
|
|
893
1139
|
}, true);
|
|
894
1140
|
```
|
|
895
1141
|
|
|
1142
|
+
A fetchable-loader POST is not a server action: the router does not run a
|
|
1143
|
+
revalidation render afterwards and the client does not invalidate its caches.
|
|
1144
|
+
Other loaders on the page keep their current data until you refresh them
|
|
1145
|
+
(`load()`, `useRefreshLoaders()`, `router.refresh()`), and cached history or
|
|
1146
|
+
prefetch entries stay valid unless you call `invalidateClientCache()`. For
|
|
1147
|
+
writes that should refresh the page, prefer a server action (`/server-actions`).
|
|
1148
|
+
|
|
896
1149
|
### File Upload Example
|
|
897
1150
|
|
|
898
1151
|
```typescript
|
|
@@ -904,9 +1157,11 @@ export const FileUploadLoader = createLoader(async (ctx) => {
|
|
|
904
1157
|
if (file && file.size > 0) {
|
|
905
1158
|
// Save to R2, D1, etc.
|
|
906
1159
|
await ctx.env.BUCKET.put(file.name, file.stream());
|
|
907
|
-
return {
|
|
1160
|
+
return {
|
|
1161
|
+
uploadedFile: { name: file.name, size: file.size, type: file.type },
|
|
1162
|
+
};
|
|
908
1163
|
}
|
|
909
|
-
return {
|
|
1164
|
+
return { uploadedFile: null };
|
|
910
1165
|
}, true);
|
|
911
1166
|
```
|
|
912
1167
|
|
|
@@ -923,7 +1178,7 @@ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
|
923
1178
|
|
|
924
1179
|
```typescript
|
|
925
1180
|
// loaders/shop.ts
|
|
926
|
-
import { createLoader } from "@rangojs/router";
|
|
1181
|
+
import { createLoader, notFound } from "@rangojs/router";
|
|
927
1182
|
|
|
928
1183
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
929
1184
|
const product = await ctx.env.DB
|
|
@@ -947,12 +1202,15 @@ export const CartLoader = createLoader(async (ctx) => {
|
|
|
947
1202
|
});
|
|
948
1203
|
|
|
949
1204
|
// urls.tsx — register loaders in the DSL
|
|
1205
|
+
import { urls } from "@rangojs/router";
|
|
950
1206
|
import * as CartActions from "./actions/cart";
|
|
1207
|
+
import { CartLoader, ProductLoader } from "./loaders/shop";
|
|
951
1208
|
|
|
952
1209
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
953
1210
|
layout(<ShopLayout />, () => [
|
|
954
1211
|
loader(CartLoader, () => [
|
|
955
|
-
|
|
1212
|
+
// after an action, re-run only for cart actions; on navigation, keep the default
|
|
1213
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
|
|
956
1214
|
]),
|
|
957
1215
|
|
|
958
1216
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|