@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.
Files changed (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -1,15 +1,20 @@
1
1
  ---
2
2
  name: loader
3
- description: Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (ssr:false).
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
- Loaders fetch data on the server and stream it to the client. For mutations
10
- (writes triggered by forms or buttons), use server actions instead — see
11
- `/server-actions`. Loaders re-resolve after an action runs, so the typical
12
- flow is _action mutates → loader re-reads → UI updates_.
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** — they resolve fresh on every
123
- request, even when the route is inside a `cache()` boundary. The router
124
- excludes them from the segment cache at storage time and re-resolves them
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
- When you register with `loader()` in the DSL, `ctx.use()` returns the
150
- same memoized result — loaders never run twice per request.
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 the same context shape as route handlers.
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 | Type | Notes |
183
- | -------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
- | `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
185
- | `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
186
- | `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
187
- | `url` | `URL` | Parsed request URL. |
188
- | `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
189
- | `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
190
- | `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
191
- | `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
192
- | `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`. |
193
- | `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`. |
194
- | `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). |
195
- | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
196
- | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
197
- | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
198
- | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
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 revalidates after cart actions (defer otherwise — keeps the
279
- // permissive loader defaults for navigation and other actions intact)
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) => ctx.isAction(CartActions) || undefined),
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 can return one of four shapes. The chain
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-type default
322
- (e.g. params-changed for routes, `false` for parallels) is used.
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
- This matters most for loaders, whose defaults are permissive: a loader defaults
339
- to revalidating on **any** action (`POST`) and on **param/search changes**
340
- during navigation. So `?? false` on a loader silently suppresses both — the
341
- loader will not refetch when you navigate to a different `:id`. Use
342
- `|| undefined` when you want to _add_ a revalidation signal on top of the
343
- sensible defaults, and reserve `?? false` for the rare case where you genuinely
344
- want the loader to refetch on nothing but your matched action.
345
-
346
- When **composing multiple revalidators** on one segment (see below), defer is
347
- mandatory: the first hard `?? false` ends the chain and the later contracts
348
- never run.
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
- revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
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
- revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
364
- revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
365
- revalidate((ctx) => ctx.isAction({ addToCart, removeFromCart }) || undefined); // object form
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
- pair it with `|| undefined` for the usual "revalidate on match, else defer"
372
- intent. It returns `false` on plain navigation and on non-matches, and resolves
373
- the reference the same way the router derives `actionId` (`$id` in production,
374
- `$$id` in dev), so it matches in both modes. The raw `actionId` string stays
375
- available on the same context as an escape hatch.
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
- revalidate(revalidateAccountScope), // producer reruns
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
- revalidate(revalidateAccountScope), // consumer reruns
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 are the live data layer of the router. They resolve fresh on every
422
- request, even when the route's UI segments are served from cache. This is a
423
- core design principle — route-level `cache()` caches rendered components but
424
- never caches loader data. Loaders are excluded at storage time and re-resolved
425
- on retrieval.
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
- This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
428
- follows the same rule: at build time, loaders are skipped entirely (there is no
429
- real request context), and at runtime the worker resolves them fresh against
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()`. Each HTML request
466
- then prints a shared-axis waterfall (and emits a `Server-Timing` header):
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.40ms 8.70ms ssr-render-html |.....##############.....................|
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-render-html` / `render:total`, not sit alone to the right of
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-render-html`. Flag a regression when a `loader:*`
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 at two levels:
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:{id}:{pathname}:{params}`
644
+ 3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
531
645
 
532
- If a custom key function throws, it falls back to the default key silently
533
- (logged to console.error).
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
- handler config (`cache.store`).
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. RSC serialization
625
- uses `toJSON()` to send only `{ __brand, $$id }` — the loader function is stripped.
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 — `<MetaTags />`,
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 capture is the BAKE lane for flagged loaders (`/ppr`): the capture
793
- render awaits them too, and the settled result — handle pushes included —
794
- freezes into the stored shell. Unflagged loaders stay masked as live
795
- holes. The `progressiveChunkSize` auto-raise is live-document only;
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. `LoaderOptions` is exported from the
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 during SSR and navigation. Pass `true` as the second
805
- argument to `createLoader` to make a loader **fetchable** — callable from the client
806
- via `useFetchLoader()` and `load()`:
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 `_rsc_loader` fetch requests (client-side
844
- `load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
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 { uploaded: { name: file.name, size: file.size, type: file.type } };
1038
+ return {
1039
+ uploadedFile: { name: file.name, size: file.size, type: file.type },
1040
+ };
908
1041
  }
909
- return { uploaded: null };
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
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
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" }, () => [