@rangojs/router 0.16.0 → 0.17.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 +254 -89
- package/dist/testing/vitest.js +252 -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-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- 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/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/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- 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 +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- 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/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- 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/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 +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- 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/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -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 +39 -33
- 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 +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -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 +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- 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 +8 -5
- 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/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 +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- 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 +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- 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 +1 -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 +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- 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 +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- 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 +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -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 +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -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 +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
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, streams its
|
|
11
|
+
result to the client, and client components read it with `useLoader()`. This
|
|
12
|
+
skill covers defining, registering, consuming, revalidating, caching, and
|
|
13
|
+
client-fetching loaders.
|
|
14
|
+
|
|
15
|
+
For mutations (writes triggered by forms or buttons), use server actions
|
|
16
|
+
instead — see `/server-actions`. Loaders re-resolve after an action runs, so
|
|
17
|
+
the typical flow is _action mutates → loader re-reads → UI updates_.
|
|
13
18
|
|
|
14
19
|
## Not this skill if…
|
|
15
20
|
|
|
@@ -119,17 +124,22 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
119
124
|
> - `revalidate()` — which **server** segments/loaders recompute during
|
|
120
125
|
> navigation and action refreshes.
|
|
121
126
|
|
|
122
|
-
DSL loaders are the **live data layer
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
on retrieval. This means `cache()` gives you cached UI + fresh data by
|
|
126
|
-
default.
|
|
127
|
+
DSL loaders are the **live data layer**: they resolve fresh on every request,
|
|
128
|
+
even when the route is inside a `cache()` boundary, so `cache()` gives you
|
|
129
|
+
cached UI + fresh data by default. See "Loaders: The Live Data Layer" below.
|
|
127
130
|
|
|
128
131
|
### Cache safety
|
|
129
132
|
|
|
130
133
|
DSL loaders can safely read `createVar({ cache: false })` variables
|
|
131
134
|
because they are always resolved fresh. The read guard is bypassed for
|
|
132
|
-
loader functions — they never produce stale data.
|
|
135
|
+
loader functions — they never produce stale data. A `"use cache"` function
|
|
136
|
+
the loader calls is not a loader body: a non-cacheable read inside it throws,
|
|
137
|
+
also through the loader's `ctx` passed in, so read the var in the loader and
|
|
138
|
+
pass the value in as an argument. Passing the loader's `ctx` itself is fine:
|
|
139
|
+
the call keys by route, params and search, like a handler `ctx` (`/use-cache`).
|
|
140
|
+
A fetchable loader called with a request body (`load({ method: "POST", body })`
|
|
141
|
+
or form data) is the exception: the body is not in the key, so the call runs
|
|
142
|
+
uncached. Pass the body fields the function needs as arguments instead.
|
|
133
143
|
|
|
134
144
|
### ctx.use(Loader) — escape hatch
|
|
135
145
|
|
|
@@ -137,6 +147,7 @@ For cases where you need loader data in the server handler itself (e.g.,
|
|
|
137
147
|
to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
|
|
138
148
|
|
|
139
149
|
```typescript
|
|
150
|
+
// Product is a context-variable token: export const Product = createVar<ProductData>();
|
|
140
151
|
path("/product/:slug", async (ctx) => {
|
|
141
152
|
const { product } = await ctx.use(ProductLoader);
|
|
142
153
|
ctx.set(Product, product); // make available to children
|
|
@@ -146,8 +157,14 @@ path("/product/:slug", async (ctx) => {
|
|
|
146
157
|
])
|
|
147
158
|
```
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
`ctx.use(Loader)` is a live read, memoized for the request: however many
|
|
161
|
+
times it is called, and whether or not the loader is also registered with
|
|
162
|
+
`loader()`, it runs once per request. It never reads or writes the loader
|
|
163
|
+
store cache on its own: a loader's `cache()` belongs to its DSL binding
|
|
164
|
+
(`loader(Def, () => [cache()])` on a route, layout or intercept). A handler
|
|
165
|
+
read of a cached binding gets the binding's (possibly cached) result once the
|
|
166
|
+
binding has started (a read from its own entry's handler or from later in
|
|
167
|
+
the tree).
|
|
151
168
|
|
|
152
169
|
**Limitations of ctx.use(Loader):**
|
|
153
170
|
|
|
@@ -175,27 +192,34 @@ same memoized result — loaders never run twice per request.
|
|
|
175
192
|
|
|
176
193
|
## Loader Context
|
|
177
194
|
|
|
178
|
-
Loaders receive
|
|
195
|
+
Loaders receive a request-scoped context that shares its read surface with
|
|
196
|
+
route handlers (`params`, `request`, `url`, `env`, `get`, `use`, `reverse`).
|
|
197
|
+
It has no handler-only writers: there is no `ctx.set`, `ctx.headers`, or
|
|
198
|
+
`ctx.setLocationState` (a loader can stream after the response has started,
|
|
199
|
+
so response-shaping belongs in middleware or the handler).
|
|
179
200
|
|
|
180
201
|
### Full field surface
|
|
181
202
|
|
|
182
|
-
| Field
|
|
183
|
-
|
|
|
184
|
-
| `params`
|
|
185
|
-
| `routeParams`
|
|
186
|
-
| `request`
|
|
187
|
-
| `url`
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
203
|
+
| Field | Type | Notes |
|
|
204
|
+
| ------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
205
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
206
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
207
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
208
|
+
| `url` | `URL` | Request URL with internal `_rsc*` transport params stripped. Use this for application logic. |
|
|
209
|
+
| `originalUrl` | `URL` | Request URL with every param intact, including `_rsc*`. Only for debugging or custom cache keying. |
|
|
210
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
211
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
212
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
213
|
+
| `env` | `any` | Bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). `createLoader` types it as `any`; annotate or cast at the use site. |
|
|
214
|
+
| `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. |
|
|
215
|
+
| `executionContext` | `ExecutionContext \| undefined` | Raw Cloudflare `ExecutionContext` for libraries that need it; `undefined` off Cloudflare. Prefer `waitUntil`. |
|
|
216
|
+
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`. |
|
|
217
|
+
| `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`. |
|
|
218
|
+
| `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). |
|
|
219
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
220
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
221
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
222
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
199
223
|
|
|
200
224
|
### Example
|
|
201
225
|
|
|
@@ -275,10 +299,13 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
275
299
|
revalidate(() => false), // Never revalidate
|
|
276
300
|
]),
|
|
277
301
|
|
|
278
|
-
// Loader that
|
|
279
|
-
//
|
|
302
|
+
// Loader that re-runs only after cart actions. Loaders re-run after every
|
|
303
|
+
// action by default; on navigation, `undefined` keeps the default
|
|
304
|
+
// (re-run when params or search change).
|
|
280
305
|
loader(CartLoader, () => [
|
|
281
|
-
revalidate((ctx) =>
|
|
306
|
+
revalidate((ctx) =>
|
|
307
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
308
|
+
),
|
|
282
309
|
]),
|
|
283
310
|
]);
|
|
284
311
|
```
|
|
@@ -292,7 +319,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
292
319
|
> Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
|
|
293
320
|
> See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
294
321
|
|
|
295
|
-
A `revalidate(fn)` callback
|
|
322
|
+
A `revalidate(fn)` callback returns one of three kinds of value. The chain
|
|
296
323
|
processes revalidators in order; each call's return controls how the
|
|
297
324
|
chain continues:
|
|
298
325
|
|
|
@@ -318,8 +345,24 @@ revalidate(() => undefined); // explicit defer
|
|
|
318
345
|
revalidate(() => null); // explicit defer
|
|
319
346
|
```
|
|
320
347
|
|
|
321
|
-
If every revalidator on a segment defers, the segment
|
|
322
|
-
(
|
|
348
|
+
If every revalidator on a segment defers, the segment default is used
|
|
349
|
+
(`defaultShouldRevalidate`, see the table below).
|
|
350
|
+
|
|
351
|
+
The callback must be **synchronous**. A returned Promise is ignored (dev
|
|
352
|
+
warning) and the running suggestion is kept, so move async work into a loader.
|
|
353
|
+
A callback that throws is logged and treated as a defer; a thrown `Response`
|
|
354
|
+
(e.g. `throw redirect(...)`) propagates as control flow.
|
|
355
|
+
|
|
356
|
+
Segment defaults when no revalidator decides (from
|
|
357
|
+
`src/router/revalidation.ts`). A segment the client does not hold yet always
|
|
358
|
+
renders.
|
|
359
|
+
|
|
360
|
+
| Segment | After an action (POST) | Navigation |
|
|
361
|
+
| -------------------------------------------------------------------- | ---------------------- | ----------------------------------- |
|
|
362
|
+
| Route (`path()`) | `true` | `true` when params or search change |
|
|
363
|
+
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true` | `true` when params or search change |
|
|
364
|
+
| Loader above the route (e.g. on a parent `layout()`) | `true` | `false` |
|
|
365
|
+
| Layout or parallel above the route | `false` | `false` |
|
|
323
366
|
|
|
324
367
|
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
325
368
|
|
|
@@ -335,17 +378,28 @@ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
|
|
|
335
378
|
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
336
379
|
```
|
|
337
380
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
381
|
+
Pick the idiom by what the segment's default already does (table above). After
|
|
382
|
+
an action, loaders, the route, and segments inside the `path()` already re-run;
|
|
383
|
+
layouts and parallels above the route are skipped:
|
|
384
|
+
|
|
385
|
+
- **Add a signal where the default skips.** `ctx.isAction(X) || undefined` on a
|
|
386
|
+
parent layout or a layout-level parallel re-renders it after `X` and defers
|
|
387
|
+
otherwise. On a loader, the route, or a segment inside the `path()` it
|
|
388
|
+
changes nothing: they already re-run after every action, and on navigation it
|
|
389
|
+
defers.
|
|
390
|
+
- **Narrow after actions where the default re-runs.**
|
|
391
|
+
`ctx.isAction() ? ctx.isAction(X) : undefined` re-runs a loader or route
|
|
392
|
+
segment only for `X`'s actions and keeps the navigation default (params or
|
|
393
|
+
search changed). After an action it is a hard decision, so later revalidators
|
|
394
|
+
on the same segment do not run for actions.
|
|
395
|
+
- **Avoid bare `ctx.isAction(X)` (and `?? false`).** It returns a hard `false`
|
|
396
|
+
on navigation too, so a loader stops refetching when you navigate to a
|
|
397
|
+
different `:id`. Reserve it for the rare case where the segment should re-run
|
|
398
|
+
on nothing but the matched action.
|
|
399
|
+
|
|
400
|
+
When **composing multiple revalidators** on one segment (see below), prefer the
|
|
401
|
+
defer form: the first hard `false` ends the chain and the later contracts never
|
|
402
|
+
run.
|
|
349
403
|
|
|
350
404
|
#### Matching actions: `ctx.isAction()`
|
|
351
405
|
|
|
@@ -358,21 +412,31 @@ import { addToCart, removeFromCart } from "../actions/cart";
|
|
|
358
412
|
import * as CartActions from "../actions/cart";
|
|
359
413
|
|
|
360
414
|
loader(CartLoader, () => [
|
|
361
|
-
|
|
415
|
+
// After an action, re-run only for addToCart; on navigation, keep the default.
|
|
416
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(addToCart) : undefined)),
|
|
362
417
|
]);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
418
|
+
|
|
419
|
+
// Matcher forms (each returns a boolean):
|
|
420
|
+
ctx.isAction(addToCart); // one action
|
|
421
|
+
ctx.isAction(addToCart, removeFromCart); // several
|
|
422
|
+
ctx.isAction(CartActions); // any action in the module
|
|
423
|
+
ctx.isAction({ addToCart, removeFromCart }); // object form
|
|
424
|
+
ctx.isAction({ Cart: CartActions }); // grouped namespaces
|
|
425
|
+
ctx.isAction(); // no args: any action at all
|
|
366
426
|
```
|
|
367
427
|
|
|
368
428
|
`isAction()` is a method on the revalidate predicate's **context argument** —
|
|
369
429
|
there is no standalone `isAction` import; you always reach it through the callback
|
|
370
430
|
parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
431
|
+
wrap it by placement (the three cases above): the ternary on a loader or route
|
|
432
|
+
segment, `|| undefined` on a layout or layout-level parallel the default skips.
|
|
433
|
+
It returns `false` on plain navigation and on non-matches; called with no
|
|
434
|
+
arguments it answers "is this request an action?". It resolves the reference with
|
|
435
|
+
the same `$id ?? $$id` precedence the router uses to derive `actionId`, so it
|
|
436
|
+
matches in both dev and production. The raw `actionId` string stays available on
|
|
437
|
+
the same context as an escape hatch, alongside `actionResult`, `formData`,
|
|
438
|
+
`actionUrl`, `currentParams`/`nextParams`, `currentUrl`/`nextUrl`,
|
|
439
|
+
`fromRouteName`/`toRouteName`, and the full handler `context`.
|
|
376
440
|
|
|
377
441
|
### Revalidation Contracts for Loader Dependencies
|
|
378
442
|
|
|
@@ -381,18 +445,23 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
381
445
|
|
|
382
446
|
```typescript
|
|
383
447
|
// revalidation-contracts.ts
|
|
448
|
+
import type { Revalidate } from "@rangojs/router";
|
|
384
449
|
import * as AccountActions from "./actions/account";
|
|
385
450
|
|
|
386
451
|
// Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
|
|
387
452
|
// so these contracts compose — a hard `false` would short-circuit the rest.
|
|
388
|
-
export const revalidateAccountScope = (ctx) =>
|
|
453
|
+
export const revalidateAccountScope: Revalidate = (ctx) =>
|
|
389
454
|
ctx.isAction(AccountActions) || undefined;
|
|
390
455
|
|
|
456
|
+
// urls.tsx
|
|
391
457
|
layout(AccountLayout, () => [
|
|
392
|
-
|
|
458
|
+
// producer: adds the re-render the default skips for a parent layout
|
|
459
|
+
revalidate(revalidateAccountScope),
|
|
393
460
|
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
394
461
|
loader(OrdersLoader, () => [
|
|
395
|
-
|
|
462
|
+
// consumer: loaders re-run after every action anyway; the shared
|
|
463
|
+
// contract names the dependency
|
|
464
|
+
revalidate(revalidateAccountScope),
|
|
396
465
|
]),
|
|
397
466
|
]),
|
|
398
467
|
]);
|
|
@@ -418,16 +487,15 @@ layout(AccountLayout, () => [
|
|
|
418
487
|
|
|
419
488
|
## Loaders: The Live Data Layer
|
|
420
489
|
|
|
421
|
-
Loaders
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
490
|
+
Loaders resolve fresh on every request, even when the route's UI segments are
|
|
491
|
+
served from cache. Route-level `cache()` caches rendered segments but never
|
|
492
|
+
loader data: loaders are excluded when the segments are stored and re-resolved
|
|
493
|
+
when they are served. Caching a loader's own data is a separate opt-in (see
|
|
494
|
+
"Opting a Loader into Caching").
|
|
426
495
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
the live database.
|
|
496
|
+
Pre-rendering follows the same rule: at build time loaders are skipped entirely
|
|
497
|
+
(there is no real request context), and at runtime the worker resolves them
|
|
498
|
+
fresh against the live database.
|
|
431
499
|
|
|
432
500
|
### Parallel and streaming — latency overlaps first paint
|
|
433
501
|
|
|
@@ -462,16 +530,26 @@ const router = createRouter({ document: Document, debugPerformance: true });
|
|
|
462
530
|
```
|
|
463
531
|
|
|
464
532
|
Or enable it per-request from middleware (e.g. only when `?debug` is present) by
|
|
465
|
-
calling `ctx.debugPerformance()` **before** `await next()
|
|
466
|
-
|
|
533
|
+
calling `ctx.debugPerformance()` **before** `await next()`:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
router.use(async (ctx, next) => {
|
|
537
|
+
if (ctx.searchParams.has("debug")) ctx.debugPerformance();
|
|
538
|
+
return next();
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Each metered request then prints a shared-axis waterfall to the server console
|
|
543
|
+
(and adds the timings to the `Server-Timing` response header):
|
|
467
544
|
|
|
468
545
|
```
|
|
469
546
|
[RSC Perf] GET /product/widget (24.53ms)
|
|
470
547
|
start dur span timeline
|
|
471
548
|
0.08ms 3.20ms route-matching |#####...................................|
|
|
472
|
-
3.
|
|
549
|
+
3.30ms 20.10ms render:total:product |....############################........|
|
|
550
|
+
3.40ms 8.70ms ssr:render-html |.....##############.....................|
|
|
473
551
|
3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
|
|
474
|
-
3.45ms 11.40ms loader:…#ReviewsLoader
|
|
552
|
+
3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
|
|
475
553
|
0.00ms 24.53ms handler:total |########################################|
|
|
476
554
|
```
|
|
477
555
|
|
|
@@ -479,12 +557,13 @@ How to read it:
|
|
|
479
557
|
|
|
480
558
|
- **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
|
|
481
559
|
offset and run side by side are executing **in parallel** — loaders should
|
|
482
|
-
overlap `ssr
|
|
560
|
+
overlap `ssr:render-html` / `render:total`, not sit alone to the right of
|
|
483
561
|
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
|
|
562
|
+
chase. `handler:total` is the whole request; `render:total` is the render pass
|
|
563
|
+
(labelled `render:total:<routeName>` when the matched route has a name).
|
|
485
564
|
- **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
|
|
486
565
|
overlaps paint when its `[start, start+dur]` interval intersects
|
|
487
|
-
`render:total` / `ssr
|
|
566
|
+
`render:total` / `ssr:render-html`. Flag a regression when a `loader:*`
|
|
488
567
|
interval is **disjoint from and starts after** `render:total`, or when its
|
|
489
568
|
`dur` approaches `handler:total` — that loader is on the critical path instead
|
|
490
569
|
of overlapping it. Two `loader:*` rows with near-equal `start` confirm
|
|
@@ -498,6 +577,10 @@ To cache a specific loader's data, attach a `cache()` child:
|
|
|
498
577
|
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
499
578
|
```
|
|
500
579
|
|
|
580
|
+
This works wherever the DSL binding is declared, an intercept's `use()`
|
|
581
|
+
included. A handler's `ctx.use(Loader)` read never caches by itself (see
|
|
582
|
+
"ctx.use(Loader) — escape hatch").
|
|
583
|
+
|
|
501
584
|
The loader's data is cached independently from the route's segment cache,
|
|
502
585
|
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
503
586
|
|
|
@@ -505,12 +588,43 @@ Values are serialized through RSC Flight, so loaders can return ReactNode,
|
|
|
505
588
|
Promises, null, and any RSC-serializable type — all round-trip correctly
|
|
506
589
|
through the cache.
|
|
507
590
|
|
|
591
|
+
A value Flight cannot encode cleanly is not cached: a promise in it that
|
|
592
|
+
rejects, an async component that throws, a function or a class instance.
|
|
593
|
+
Storing it would bake the failure into every hit until expiry. Instead the
|
|
594
|
+
write is skipped and reported to `onError` (phase `"cache"`, category
|
|
595
|
+
`cache-write`, or `stale-revalidation` for a background refresh): a miss still
|
|
596
|
+
returns the loader's live result, the next request runs the loader again, and
|
|
597
|
+
a stale entry keeps serving.
|
|
598
|
+
|
|
599
|
+
Handle pushes the body makes (`ctx.use(Meta)(...)`, breadcrumbs — see
|
|
600
|
+
"Writing Handles from Loaders"), including those of loaders it awaits via
|
|
601
|
+
`ctx.use`, are stored with the value and replayed on every hit, stale hits
|
|
602
|
+
included: appended to the loader's segment in push order, so a cached
|
|
603
|
+
loader's title or crumb stays on the page. Relative to the handler's and
|
|
604
|
+
sibling loaders' pushes, the replay lands when the cache read completes —
|
|
605
|
+
the same race a live loader push has. A loader the cached body awaited can
|
|
606
|
+
still run on a hit, because a sibling loader or the handler also reads it
|
|
607
|
+
(loaders stay live); its pushes still appear once, and they are the live
|
|
608
|
+
ones. If it ran before the replay, the replay skips its recorded pushes; if
|
|
609
|
+
it runs after, its pushes replace the replayed ones in their position (after
|
|
610
|
+
hydration, when it lands after the document's handle snapshot). A pending push
|
|
611
|
+
(a promise, an async callback, a `.defer()` slot) is recorded once it settles,
|
|
612
|
+
as in `"use cache"`: the background write waits up to 5 s, and a push still
|
|
613
|
+
pending at 5 s drops that entry's handle record (the data is still cached; its
|
|
614
|
+
hits replay no pushes). A pushed value Flight cannot encode (a promise that
|
|
615
|
+
rejects) skips the whole entry, as above. Entries written
|
|
616
|
+
before handles were recorded replay none until they are rewritten (expiry or
|
|
617
|
+
SWR revalidation).
|
|
618
|
+
|
|
508
619
|
### Cache Key
|
|
509
620
|
|
|
510
|
-
The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}
|
|
511
|
-
This can be customized
|
|
621
|
+
The default cache key is `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
622
|
+
(the host keeps multi-tenant hosts from sharing entries). This can be customized
|
|
623
|
+
at two levels:
|
|
512
624
|
|
|
513
625
|
```typescript
|
|
626
|
+
import { cookies } from "@rangojs/router";
|
|
627
|
+
|
|
514
628
|
// Full override — key function replaces the default entirely
|
|
515
629
|
loader(ProductLoader, () => [
|
|
516
630
|
cache({
|
|
@@ -527,10 +641,12 @@ Resolution priority (same as route-level `cache()`):
|
|
|
527
641
|
|
|
528
642
|
1. `key(ctx)` from cache options — full override
|
|
529
643
|
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
|
|
530
|
-
3. Default key — `loader:{
|
|
644
|
+
3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
531
645
|
|
|
532
|
-
|
|
533
|
-
|
|
646
|
+
A `key` function (or store `keyGenerator`) that throws is **not** caught: the
|
|
647
|
+
loader fails as if its body threw. There is no silent fallback to the default
|
|
648
|
+
key, because a personalised key collapsing onto the broad default would share
|
|
649
|
+
one user's data with everyone.
|
|
534
650
|
|
|
535
651
|
### Tags for Invalidation
|
|
536
652
|
|
|
@@ -589,8 +705,9 @@ loader(PricingLoader, () => [
|
|
|
589
705
|
]),
|
|
590
706
|
```
|
|
591
707
|
|
|
592
|
-
Without an explicit store, the loader uses the app-level store from the
|
|
593
|
-
|
|
708
|
+
Without an explicit store, the loader uses the app-level store from the router
|
|
709
|
+
config (`createRouter({ cache: { store } })`). Without any store, `cache()` on
|
|
710
|
+
a loader is inert and the loader runs fresh.
|
|
594
711
|
|
|
595
712
|
## Multiple Loaders
|
|
596
713
|
|
|
@@ -621,8 +738,9 @@ layout(<ShopLayout />, () => [
|
|
|
621
738
|
|
|
622
739
|
## Passing Loaders as Props
|
|
623
740
|
|
|
624
|
-
Loaders can be passed as props from server to client components.
|
|
625
|
-
|
|
741
|
+
Loaders can be passed as props from server to client components. A loader
|
|
742
|
+
definition is a plain `{ __brand, $$id }` object (the function itself stays in the
|
|
743
|
+
server registry), so it serializes as-is.
|
|
626
744
|
|
|
627
745
|
```typescript
|
|
628
746
|
// Server component (route handler)
|
|
@@ -701,8 +819,6 @@ flushes, so both signals are resolved server-side while the tree is built:
|
|
|
701
819
|
document, the client replaces to the target on hydration) and `notFound()`
|
|
702
820
|
renders the not-found UI at the owning segment with a real 404. No read site
|
|
703
821
|
runs, so a `useLoader` in a layout above every Suspense boundary is safe.
|
|
704
|
-
(Before this was fixed, the flagged read threw inside the Fizz shell and
|
|
705
|
-
500ed the document.)
|
|
706
822
|
|
|
707
823
|
Session/auth gates belong in middleware (they are request-shaped, not
|
|
708
824
|
data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
|
|
@@ -731,12 +847,17 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
731
847
|
```
|
|
732
848
|
|
|
733
849
|
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 — `<
|
|
850
|
+
barrier ride the SSR handle snapshot (in the SSR'd document — `<Html.Meta />`,
|
|
735
851
|
`useHandle` reads); later pushes stream to the client and apply post-hydration
|
|
736
852
|
on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
737
853
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
738
854
|
the fetched data usually does not. When it MUST be in the document, use
|
|
739
|
-
`ssr: false` below.
|
|
855
|
+
`ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
|
|
856
|
+
they are replayed from the entry (see "Opting a Loader into Caching").
|
|
857
|
+
|
|
858
|
+
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
859
|
+
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
860
|
+
settled `ssr: false` pushes in the shell record — see `/ppr`.)
|
|
740
861
|
|
|
741
862
|
Reads are the other direction and gated: `ctx.get(handle)` throws unless the
|
|
742
863
|
loader first does `await ctx.rendered()` (DSL-registered loaders only —
|
|
@@ -759,6 +880,9 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
759
880
|
]),
|
|
760
881
|
```
|
|
761
882
|
|
|
883
|
+
The options object is typed as `LoaderOptions`
|
|
884
|
+
(`import type { LoaderOptions } from "@rangojs/router"`).
|
|
885
|
+
|
|
762
886
|
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
|
|
763
887
|
for this loader, so nothing of it is left to stream in the document:
|
|
764
888
|
document renders await this loader before first flush — data is settled
|
|
@@ -789,21 +913,20 @@ The costs and constraints:
|
|
|
789
913
|
document render awaits the loader before the render barrier resolves, so
|
|
790
914
|
that wait is a cycle by construction; it throws a deadlock error naming the
|
|
791
915
|
fix.
|
|
792
|
-
- PPR
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
captured shells outline per the explicit option or React's default.
|
|
916
|
+
- Under PPR the flag is also the bake lane: the settled non-promise data
|
|
917
|
+
(handle pushes included) freezes into the stored shell — see `/ppr` → The
|
|
918
|
+
loader lane rule. The `progressiveChunkSize` auto-raise is live-document
|
|
919
|
+
only; captured shells outline per the explicit option or React's default.
|
|
797
920
|
|
|
798
921
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
799
|
-
loader-heavy shape makes it most useful.
|
|
800
|
-
package root.
|
|
922
|
+
loader-heavy shape makes it most useful.
|
|
801
923
|
|
|
802
924
|
## Fetchable Loaders
|
|
803
925
|
|
|
804
|
-
By default, loaders only run
|
|
805
|
-
|
|
806
|
-
|
|
926
|
+
By default, loaders only run as part of a render (document, navigation, or
|
|
927
|
+
post-action revalidation); the `_rsc_loader` endpoint rejects them. Pass `true`
|
|
928
|
+
as the second argument to `createLoader` to make a loader **fetchable** —
|
|
929
|
+
callable from the client via `useFetchLoader()` and `load()`:
|
|
807
930
|
|
|
808
931
|
```typescript
|
|
809
932
|
import { createLoader } from "@rangojs/router";
|
|
@@ -839,9 +962,10 @@ requireAuth] })` (next section).
|
|
|
839
962
|
|
|
840
963
|
### Fetchable Loader with Middleware
|
|
841
964
|
|
|
842
|
-
Pass an options object instead of `true` to attach per-loader middleware
|
|
843
|
-
This middleware runs only on
|
|
844
|
-
`
|
|
965
|
+
Pass an options object instead of `true` to attach per-loader middleware (any
|
|
966
|
+
options object makes the loader fetchable). This middleware runs only on
|
|
967
|
+
`_rsc_loader` fetch requests (client-side `load()` / `useFetchLoader()` /
|
|
968
|
+
`useRefreshLoaders()` calls), not when the loader runs as part of a render:
|
|
845
969
|
|
|
846
970
|
```typescript
|
|
847
971
|
import { createLoader } from "@rangojs/router";
|
|
@@ -893,6 +1017,13 @@ export const MutationLoader = createLoader(async (ctx) => {
|
|
|
893
1017
|
}, true);
|
|
894
1018
|
```
|
|
895
1019
|
|
|
1020
|
+
A fetchable-loader POST is not a server action: the router does not run a
|
|
1021
|
+
revalidation render afterwards and the client does not invalidate its caches.
|
|
1022
|
+
Other loaders on the page keep their current data until you refresh them
|
|
1023
|
+
(`load()`, `useRefreshLoaders()`, `router.refresh()`), and cached history or
|
|
1024
|
+
prefetch entries stay valid unless you call `invalidateClientCache()`. For
|
|
1025
|
+
writes that should refresh the page, prefer a server action (`/server-actions`).
|
|
1026
|
+
|
|
896
1027
|
### File Upload Example
|
|
897
1028
|
|
|
898
1029
|
```typescript
|
|
@@ -904,9 +1035,11 @@ export const FileUploadLoader = createLoader(async (ctx) => {
|
|
|
904
1035
|
if (file && file.size > 0) {
|
|
905
1036
|
// Save to R2, D1, etc.
|
|
906
1037
|
await ctx.env.BUCKET.put(file.name, file.stream());
|
|
907
|
-
return {
|
|
1038
|
+
return {
|
|
1039
|
+
uploadedFile: { name: file.name, size: file.size, type: file.type },
|
|
1040
|
+
};
|
|
908
1041
|
}
|
|
909
|
-
return {
|
|
1042
|
+
return { uploadedFile: null };
|
|
910
1043
|
}, true);
|
|
911
1044
|
```
|
|
912
1045
|
|
|
@@ -923,7 +1056,7 @@ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
|
923
1056
|
|
|
924
1057
|
```typescript
|
|
925
1058
|
// loaders/shop.ts
|
|
926
|
-
import { createLoader } from "@rangojs/router";
|
|
1059
|
+
import { createLoader, notFound } from "@rangojs/router";
|
|
927
1060
|
|
|
928
1061
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
929
1062
|
const product = await ctx.env.DB
|
|
@@ -947,12 +1080,15 @@ export const CartLoader = createLoader(async (ctx) => {
|
|
|
947
1080
|
});
|
|
948
1081
|
|
|
949
1082
|
// urls.tsx — register loaders in the DSL
|
|
1083
|
+
import { urls } from "@rangojs/router";
|
|
950
1084
|
import * as CartActions from "./actions/cart";
|
|
1085
|
+
import { CartLoader, ProductLoader } from "./loaders/shop";
|
|
951
1086
|
|
|
952
1087
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
953
1088
|
layout(<ShopLayout />, () => [
|
|
954
1089
|
loader(CartLoader, () => [
|
|
955
|
-
|
|
1090
|
+
// after an action, re-run only for cart actions; on navigation, keep the default
|
|
1091
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
|
|
956
1092
|
]),
|
|
957
1093
|
|
|
958
1094
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|