@rangojs/router 0.24.3 → 0.25.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 (274) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -142,6 +142,85 @@ At the top level of the builder only `path()`, `layout()` and `intercept()`
142
142
  are accepted, and the builder must define at least one `path()`. Any helper
143
143
  name not in this table throws (`clientUrls() has no route() helper`).
144
144
 
145
+ `loader`, `loading`, `revalidate` and `transition` are also exported from
146
+ `@rangojs/router/client`, the same functions the builder hands out, for a
147
+ component's own items (next section).
148
+
149
+ ## A component's own items: `Component.use`
150
+
151
+ A page, layout or intercept component can carry its own items, as a server
152
+ handler does with `.use` (`/handler-use`): its loaders, its skeleton and its
153
+ revalidation rules travel with it instead of being repeated at every `path()`
154
+ that mounts it. Build them with the helpers from `@rangojs/router/client`. The
155
+ component's module needs no `clientUrls()`, and no `"use client"` of its own
156
+ when a group imports it.
157
+
158
+ ```tsx
159
+ // src/dash/Dashboard.tsx
160
+ "use client";
161
+ import { loader, loading, revalidate, useLoader } from "@rangojs/router/client";
162
+ import * as StatsActions from "./stats.actions";
163
+ import { StatsLoader } from "./stats.loader";
164
+
165
+ export function Dashboard() {
166
+ const { data } = useLoader(StatsLoader);
167
+ return <StatsView stats={data} />;
168
+ }
169
+ Dashboard.use = () => [
170
+ loader(StatsLoader, () => [
171
+ revalidate(({ isAction }) => isAction(StatsActions)),
172
+ ]),
173
+ loading(<DashboardSkeleton />),
174
+ ];
175
+ ```
176
+
177
+ ```tsx
178
+ // src/urls/app.client.tsx
179
+ "use client";
180
+ import { clientUrls } from "@rangojs/router/client";
181
+ import { Dashboard } from "../dash/Dashboard";
182
+
183
+ export default clientUrls(({ path }) => [
184
+ path("/dash", Dashboard, { name: "dash" }),
185
+ ]);
186
+ ```
187
+
188
+ | Component mounted by | Its `.use` may return |
189
+ | -------------------- | ---------------------------------------------- |
190
+ | `path()` | `loader()`, `loading()`, `transition()` |
191
+ | `layout()` | `loader()`, `loading()` |
192
+ | `intercept()` | `loader()` (without `ssr: false`), `loading()` |
193
+
194
+ - **Merge order is the server's.** The component's items come first and the
195
+ mount site's own items second: an explicit `loading()` or `transition()`
196
+ wins, loaders accumulate (the same definition in both gives two loaders, as
197
+ `loader(Def)` twice does). A layout component's loaders are layout loaders,
198
+ shared by every route below it.
199
+ - **No routes inside `.use`.** `path()`, `layout()` and `intercept()` there
200
+ throw (`clientUrls() does not support path() inside Dashboard.use`): route
201
+ types and the generated `.gen.ts` are read from the builder's own items.
202
+ Unlike a server layout handler's `.use`, a client layout's cannot add
203
+ routes.
204
+ - **`.use` runs when the group is defined**, once per mount site (in the
205
+ browser, during SSR and in discovery), never at render. The error for an
206
+ item the site does not take names the component.
207
+ - **A predicate in `.use` runs on the server,** as one in the builder does:
208
+ the build hoists it out of the component's module with the rules below,
209
+ and it registers when a group whose routes use the component loads on the
210
+ server, wherever the module sits: behind barrels or in another workspace
211
+ package. A predicate in `node_modules` is not hoisted,
212
+ and route discovery fails naming its loader.
213
+ - **Import the helpers from `@rangojs/router/client`.** The same names from
214
+ `@rangojs/router` are the server DSL's; called in a client module they
215
+ throw, pointing to `@rangojs/router/client`.
216
+ - In dev, an edit of a component's `.use` refreshes its groups' projections
217
+ and predicates, as an edit of the group does.
218
+ - Under `isolatedDeclarations` (a library's sources), an exported function
219
+ cannot take an assigned property: declare the component unexported,
220
+ assign `.use`, and export it through a const typed `ClientUrlComponent`.
221
+ - A `createLoader()` definition's own `.use` (`Def.use`) is not read inside
222
+ `clientUrls()` yet: put its `revalidate()` on the `loader()` call.
223
+
145
224
  `include`, `parallel`, `cache`, `middleware`, `errorBoundary`,
146
225
  `notFoundBoundary` **throw** — and that is a design position, not a gap.
147
226
  Client route groups are a PERFORMANCE surface (instant presentation, held
@@ -201,10 +280,11 @@ the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
201
280
  browser before the request goes out; there is no client-computed decision and
202
281
  no hybrid.
203
282
 
204
- You still write the predicate in the `"use client"` module. The rango Vite
205
- plugin hoists each `revalidate()` argument into a server-only module, leaves a
206
- key in the browser and SSR copies, and registers the predicate when the
207
- group's module loads on the server (a lazy `include()` stays lazy). The cost
283
+ You still write the predicate in the `"use client"` module, or in a
284
+ component's `.use`. The rango Vite plugin hoists each `revalidate()` argument
285
+ into a server-only module, leaves a key in the browser and SSR copies, and
286
+ registers the predicate when the group's module loads on the server, with
287
+ those of the modules it imports (a lazy `include()` stays lazy). The cost
208
288
  is a rule: **the argument may reference imports and globals only.** Anything
209
289
  else is a build error naming `file:line:column`, starting
210
290
  `[rango] clientUrls() revalidate() at ...: revalidate() runs on the server; ...`.
@@ -213,6 +293,14 @@ else is a build error naming `file:line:column`, starting
213
293
  (`revalidate(isCartAction)`), a namespace member (`P.cart`), a call of an
214
294
  imported factory (`makePredicate("cart")`), and globals such as `URL`,
215
295
  `Boolean` and `import.meta.env`.
296
+ - `revalidate(true)` / `revalidate(false)`, shorthand for `() => true` /
297
+ `() => false`: a boolean LITERAL stays in place (nothing to hoist). Any
298
+ other argument is hoisted whatever it evaluates to, because the build cannot
299
+ tell an imported boolean from an imported predicate: the server evaluates
300
+ it, and a boolean result is the same constant answer
301
+ (`revalidate(HOLD_SESSION)` with an imported flag works). A value that is
302
+ neither fails when the server mounts the group, naming the route and
303
+ loader.
216
304
  - A binding declared in the same `"use client"` module is rejected: move the
217
305
  predicate to its own module without `"use client"` and import it, or write it
218
306
  inline. A binding of an enclosing function is rejected: read it from the
@@ -223,7 +311,8 @@ else is a build error naming `file:line:column`, starting
223
311
  request (a cookie or a search param) and read it from the argument, or
224
312
  refresh with `useRefreshLoaders`.
225
313
  - An `async` or generator function, a spread, a second argument, no argument,
226
- or a non-function value (object, array or string literal) is rejected.
314
+ or a literal that is neither a function nor a boolean (object, array,
315
+ string, number or `null`) is rejected.
227
316
  - An import of `client-only`, or of a module whose directive prologue is
228
317
  `"use client"`, is rejected (on the server its exports are client
229
318
  references). A `"use server"` import is fine: on the server it is the real
@@ -273,10 +362,10 @@ with the ternary above instead (same rule as a server-tree loader). A bare
273
362
  `isAction(refs)` is a hard `false` on navigation too, so the loader stops
274
363
  refetching when params change.
275
364
 
276
- `intercept()` loaders inside `clientUrls()` do not apply their `revalidate()`
277
- yet (a known gap, left for a follow-up). The build still checks those
278
- predicates like any other, so one that captures a binding of the `"use client"`
279
- module fails the build although nothing runs it: import it, or drop it.
365
+ `intercept()` loaders inside `clientUrls()` take `revalidate()` the same way:
366
+ the build moves the predicate to the server, and it runs there with the same
367
+ arguments, `actionResult` included, so an action from the modal re-runs only
368
+ the loaders whose predicate says so.
280
369
 
281
370
  A group `layout()` loader revalidates as a server layout's loader does. With no
282
371
  `revalidate()` a navigation keeps it, whichever route of the layout it goes to
@@ -292,7 +381,7 @@ currentParams.slug !== nextParams.slug ? true : defaultShouldRevalidate)`.
292
381
 
293
382
  Two scars worth copying:
294
383
 
295
- - A blunt `() => false` keeps serving the OLD product on product→product
384
+ - A blunt `revalidate(false)` keeps serving the OLD product on product→product
296
385
  navigations (same route, new param). Make predicates param-sensitive:
297
386
  return `defaultShouldRevalidate` when the identifying param changed.
298
387
  - One action, per-loader outcomes: a cart badge loader revalidates on cart
@@ -312,8 +401,8 @@ Everything after that is decided on the server, while it handles that request:
312
401
  - **The browser's caches.** When the response arrives the browser marks its
313
402
  history cache stale and drops its prefetches, unless the action called
314
403
  `keepClientCache()` (`/server-actions` → "Client Cache After an Action").
315
- - **A loader that fails on its rerun** reaches your `onError` on the server and
316
- the page as an error, below.
404
+ - **A loader that fails on its rerun** reaches your `onError` on the server,
405
+ and in the browser it throws at its read, below.
317
406
 
318
407
  A form's React state survives an action's revalidation in a group, as on a
319
408
  `urls()` route: the commit renders into the mounted page instead of remounting
@@ -322,17 +411,79 @@ it, so a controlled input keeps what the user typed.
322
411
  ### A loader that fails on its rerun
323
412
 
324
413
  The action has already run when its loaders rerun, so a loader that throws
325
- there does not undo it. Where the error lands decides what the user sees:
414
+ there does not undo it. It behaves as on a navigation, on every route (a group
415
+ route or a `urls()` route, with or without `loading()`), and whether the
416
+ action was called directly or from `<form action>`, `useActionState` or
417
+ `useTransition`:
418
+
419
+ - **The call resolves** with the action's return value.
420
+ - **The loader throws in place,** at its `useLoader` read. The nearest
421
+ boundary to the read catches it: your own React error boundary around the
422
+ read, else the `errorBoundary()` around the group's `include()` mount (or
423
+ the route's, in `urls()`), else the router's root error boundary.
424
+ - **Everything outside that boundary stays,** with its React state: a form
425
+ next to the failed list keeps what the user typed. Loaders that reran
426
+ without error commit their new data in the same commit.
427
+ - **The error is reported**: to your `onError` on the server (phase
428
+ `"loader"`), and in the browser as `revalidationError` on the action's
429
+ `useAction()` state. `error` stays `null` there and `result` holds the
430
+ action's return value, so "saved, but the list could not refresh" reads
431
+ apart from "the save failed".
432
+ - **A later rerun that succeeds clears it.** An `errorBoundary()` fallback
433
+ clears on the next action or navigation that reruns the loader without
434
+ error. Your own boundary resets by its own rule: key the reset on data that
435
+ reruns with the loader and is read outside the boundary, so the reset lands
436
+ in the same render as the new data. A reset when the action settles can run
437
+ before React rendered the commit and catch the old failure again.
438
+
439
+ ```tsx
440
+ // in the group's "use client" module
441
+ function StockPanel() {
442
+ // Reruns with every save and never throws.
443
+ const { data: status } = useLoader(SaveStatusLoader);
444
+ return (
445
+ <StockBoundary resetKey={status.version}>
446
+ <StockList /> {/* useLoader(StockLoader) throws here on a failed rerun */}
447
+ </StockBoundary>
448
+ );
449
+ }
326
450
 
327
- | The route | Called directly (`await action()`) | From `<form action>`, `useActionState` or `useTransition` |
328
- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
329
- | A group route, or a `urls()` route without `loading()` | The call rejects with the loader's error (`"An error occurred"` in a build), although the action returned. Nothing of that revalidation commits: the page keeps its data and its React state. | React hands the rejection to the nearest React error boundary above the form. With none of yours, the router's root error boundary replaces the page. |
330
- | A `urls()` route with `loading()` | The call resolves with the action's result. The commit renders, the loader's read throws, and the nearest React error boundary catches it; with none of yours, the root one replaces the page. | The same. |
331
- | Under an `errorBoundary()` (for a group, around the mount) | Its fallback replaces the route: the page and its form state unmount. | The same. |
451
+ function SaveStock({ sku }: { sku: string }) {
452
+ const { revalidationError } = useAction(saveStock);
453
+ const [stale, setStale] = useState(false);
454
+ // useAction resets to idle once the action has settled: keep the notice.
455
+ useEffect(() => {
456
+ if (revalidationError) setStale(true);
457
+ }, [revalidationError]);
458
+ return (
459
+ <form action={saveStock.bind(null, sku)}>
460
+ <button>Save</button>
461
+ {stale && (
462
+ <p role="status">Saved. The stock count could not be refreshed.</p>
463
+ )}
464
+ </form>
465
+ );
466
+ }
467
+ ```
332
468
 
333
- A group route's `loading()` does not change the first row. For a failure the
334
- user can retry (a flaky upstream, a rate limit), return it as data instead of
335
- throwing, and render it where the data goes:
469
+ `StockBoundary` is an ordinary React error boundary whose
470
+ `getDerivedStateFromProps` clears its error when `resetKey` changes. A loader
471
+ that throws `notFound()` or `redirect()` is not a failure and keeps its path:
472
+ a `redirect()` (a deleted record's page) replaces the route's content and
473
+ navigates, and the layouts above keep their instance. A no-JS form post
474
+ renders the whole document after the action, so a loader that throws there
475
+ behaves as on a page load. Every refetch the router runs for an action
476
+ follows the same rule (after several actions at once, after a navigation
477
+ during it), and the action that ran it reports the error as its
478
+ `revalidationError`; with several actions at once, that is the one whose
479
+ refetch covers them all. Not covered yet: an `intercept()`'s loaders, and a
480
+ parallel slot's loaders under its own `loading()`, decoded above the slot's
481
+ content, so a boundary inside the slot does not see the error.
482
+
483
+ For a failure the user should see in place of the data without an error
484
+ boundary (a flaky upstream, a
485
+ rate limit), return it as data instead of throwing, and render it where the
486
+ data goes:
336
487
 
337
488
  ```tsx
338
489
  // stock.loader.ts
@@ -355,10 +506,7 @@ function Stock() {
355
506
  }
356
507
  ```
357
508
 
358
- The action resolves with its result, the page and its form state stay, and the
359
- next action or navigation that reruns the loader replaces the message. Keep
360
- throwing for a failure that should stop the page, with an `errorBoundary()`
361
- around the mount for it.
509
+ The next action or navigation that reruns the loader replaces the message.
362
510
 
363
511
  A failure returned as data is an ordinary value, so whatever stores the
364
512
  loader's value stores it too: a `cache()` on the loader serves it for its TTL,
@@ -370,8 +518,9 @@ data in a loader that is not cached itself.
370
518
 
371
519
  Pinned by dev + production e2e in the router repository (a group, its `urls()`
372
520
  twin, each with and without `loading()`, and the group under an
373
- `errorBoundary()`), and by a `serveShellRequest({ action })` test of the
374
- response.
521
+ `errorBoundary()`), and by `serveShellRequest({ action })` tests whose
522
+ `readAction()` reads the call's answer as the browser does: the result and
523
+ the `revalidationError`.
375
524
 
376
525
  ## Loaders are full citizens: signals and handles
377
526
 
@@ -386,7 +535,7 @@ import { createLoader, notFound, redirect } from "@rangojs/router";
386
535
 
387
536
  export const ProductLoader = createLoader(async (ctx) => {
388
537
  const moved = LEGACY_SLUGS[ctx.params.slug];
389
- if (moved) throw redirect(`/shop/product/${moved}`);
538
+ if (moved) throw redirect(ctx.reverse("shop.product", { slug: moved }));
390
539
  if (!(await exists(ctx.params.slug))) notFound(`No "${ctx.params.slug}"`);
391
540
  return getProduct(ctx.params.slug);
392
541
  });
@@ -507,7 +656,9 @@ rest of the destination; otherwise it is held. A same-route click
507
656
  that layout is new to the page (entering the group from outside).
508
657
  - `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
509
658
  on a server layout above the group defers nothing for the group's routes.
510
- - `intercept()` loaders accept the flag and ignore it, as in `urls()`.
659
+ - On an `intercept()` loader the flag works as in `urls()`: an in-group
660
+ prefetch of the intercept's target defers it, and the click opens the modal
661
+ at once with one fill for it.
511
662
  - With `ssr: false` on a `ppr` group route the flag is ignored (the bake
512
663
  lane), as on a server route.
513
664
  - Test it with `serveShellRequest` in the Flight project: mount the module
@@ -557,15 +708,27 @@ a hard load of the target URL renders the full route.
557
708
  hold; it adds the view-transition animation. Inline `<Suspense>` above each
558
709
  `useLoader` read is still the finer-grained tool when different reads on
559
710
  one route should wait independently.
711
+ - **Parameterized mounts.** A group under `include("/:locale", ...)`,
712
+ `include("/:locale?", ...)` or `include("/:accountId/:projectId", ...)`
713
+ presents at the click like one under a literal mount, and `useParams()` in
714
+ the destination carries the mount's params. A click that changes a mount
715
+ param (`/en/...` to `/de/...`) is another instance of the include: no
716
+ optimistic frame, a plain navigation. The group's intercepts open only from
717
+ one of its routes in the same mount instance. A URL the server routes
718
+ elsewhere (a root route that a group `/:slug` under `/:locale?` also
719
+ matches) never renders as a group route: the server ships the competing
720
+ routes with the group.
560
721
  - **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
561
722
  kick off in parallel, so awaiting one (`ssr: false`) gives
562
723
  same-or-faster siblings time to settle coincidentally. Do not read "it was
563
724
  in the HTML once" as a guarantee — only the flagged loader is guaranteed.
564
725
  - **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
565
726
  included) — never compare it to your own `path()` patterns; build local
566
- links with `useHref()` (`groupHref("/items/1")` composes the mount).
567
- `useRouter().push("cart")` (RELATIVE,
568
- no leading slash) resolves against the mount; `push("/x")` stays
727
+ links with `useReverse(routes)` from the group's `.gen.ts`: it composes the
728
+ mount and fills its params. `useHref()` (`groupHref("/items/1")`) and a
729
+ relative `useRouter().push("cart")` also fill `:param` mount segments from
730
+ the current params (a missing one throws, or rejects the push), but append
731
+ the rest unchecked. `push("/x")` stays
569
732
  app-absolute by design (never auto-prefixed). `useSearchParams` carries REAL
570
733
  values during SSR (live request seeds the SSR store; on ppr routes the
571
734
  capture/resume renders seed the shell key's own sorted search — search is
@@ -214,14 +214,16 @@ export const shopPatterns = urls(({ path, include }) => [
214
214
  export default shopPatterns; // async include() resolves this
215
215
  ```
216
216
 
217
- **Prefer the async form** for any route group that is a natural,
218
- independently-loadable unit (a localized section, an admin area, an API surface
219
- with heavy handlers) — it trims the eagerly-parsed entry bundle and keeps that
220
- subgraph off the cold-start path. The **eager form is still fully valid** (not
221
- deprecated): keep it for small groups, or ones that share most of their module
222
- graph with the entry (the bundler keeps shared modules common regardless, so
223
- splitting a thin group buys little). Both match identically at runtime — only the
224
- module's runtime evaluation timing differs.
217
+ **Default to the eager (sync) form.** Reach for the async form when a group is
218
+ large and rarely visited, and is a natural, independently-loadable unit (a big
219
+ localized section, a heavy admin area, an API surface with heavy handlers): it
220
+ trims the eagerly-parsed entry bundle and keeps that subgraph off the cold-start
221
+ path, at the cost of the group's first request paying its chunk import (see the
222
+ measurements below). A thin group, or one that shares most of its module graph
223
+ with the entry, gains little from splitting, since the bundler keeps shared
224
+ modules common regardless. Both match identically at runtime — only the module's
225
+ runtime evaluation timing differs. `/app-structure` shows both in whole-app
226
+ layouts.
225
227
 
226
228
  What you do NOT lose by splitting: build-time discovery `await`s the provider, so
227
229
  `href()`, `ctx.reverse()`, generated route types, and prerender still see every route
@@ -51,14 +51,14 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
51
51
 
52
52
  `handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
53
53
 
54
- | Mount site | Allowed item types |
55
- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
- | `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
- | `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
58
- | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
- | `intercept()` | `middleware`, `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `revalidate` goes on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
60
- | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
- | `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
54
+ | Mount site | Allowed item types |
55
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
+ | `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
58
+ | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
+ | `intercept()` | `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `middleware` goes on the target route (it runs for the modal too), `revalidate` on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
60
+ | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
+ | `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
62
62
 
63
63
  `route` is the item type a `path()` call produces.
64
64
 
@@ -292,7 +292,7 @@ AdminLayout.use = () => [
292
292
 
293
293
  ### Intercepts (`intercept()`)
294
294
 
295
- Intercept handlers can carry their own middleware chain, loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. Boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)).
295
+ Intercept handlers can carry their own loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. `middleware()`, boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)); the target route's middleware runs for the modal.
296
296
 
297
297
  ```typescript
298
298
  const QuickViewModal: Handler = async (ctx) => {
@@ -392,6 +392,44 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
392
392
  - **Validation runs at registration / first match**, not at handler definition. A handler doesn't know its mount site at definition time — the same handler used in a `path()` and an `intercept()` is validated against each mount's allowed-types set when registered.
393
393
  - **No silent shadowing.** If a disallowed item slips through (e.g., a layout factory returning `cache()` from a slot's `handler.use`), the runtime throws with the offending type and mount site named.
394
394
 
395
+ ## In `clientUrls()`: `Component.use`
396
+
397
+ A client route group (`/client-urls`) takes the same pattern for its
398
+ components: `Dashboard.use = () => [...]` on a page, layout or intercept
399
+ component mounted by the group's `path()`, `layout()` or `intercept()`. Build
400
+ the items with the helpers `@rangojs/router/client` exports (`loader`,
401
+ `loading`, `revalidate`, `transition`); the `@rangojs/router` ones are the
402
+ server DSL's and throw in a client module.
403
+
404
+ ```tsx
405
+ "use client";
406
+ import { loader, loading, revalidate } from "@rangojs/router/client";
407
+
408
+ Dashboard.use = () => [
409
+ loader(StatsLoader, () => [
410
+ revalidate(({ isAction }) => isAction(StatsActions)),
411
+ ]),
412
+ loading(<DashboardSkeleton />),
413
+ ];
414
+ ```
415
+
416
+ The composition rules are this skill's: the component's items first, the
417
+ mount site's explicit items second, `loading()` and `transition()` last-wins,
418
+ loaders accumulate. What differs:
419
+
420
+ | Mount site in `clientUrls()` | The component's `.use` may return |
421
+ | ---------------------------- | ------------------------------------------ |
422
+ | `path()` | `loader`, `loading`, `transition` |
423
+ | `layout()` | `loader`, `loading` |
424
+ | `intercept()` | `loader` (without `ssr: false`), `loading` |
425
+
426
+ - No `route`, `layout`, `intercept` or `include` items: a group's routes and
427
+ its generated types come from the builder's own items.
428
+ - A `revalidate()` predicate still runs on the server: the build hoists it
429
+ out of the component's module (see `/client-urls`, "Revalidation runs on
430
+ the server").
431
+ - A `createLoader()` definition's `.use` is not read inside a group yet.
432
+
395
433
  ## Cross-references
396
434
 
397
435
  - `skills/route` — `path()` mount site basics
@@ -400,3 +438,4 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
400
438
  - `skills/intercept` — intercept mount site basics
401
439
  - `skills/loader` — defining `createLoader` and reading via `ctx.use()`
402
440
  - `skills/middleware` — middleware semantics and ordering
441
+ - `skills/client-urls` — `Component.use` in a client route group
@@ -39,6 +39,107 @@ the capture, so its pushes always apply client-side; with `ssr: false` they
39
39
  bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
40
40
  re-renders on every handle update, late loader pushes included.
41
41
 
42
+ ### When handle values change (by design)
43
+
44
+ Handle values are reactive and arrive on their own channel, by design. A value
45
+ belongs to the segment that pushed it and reaches `useHandle` when that push
46
+ settles. The page never waits for a handle, and a handle never shows the next
47
+ page's values over the page being left:
48
+
49
+ | Case | What `useHandle` returns |
50
+ | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. |
52
+ | Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
53
+ | Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
54
+ | `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
55
+
56
+ You never see a pending value, a blank, or the destination's title over the old
57
+ page. A crumb whose `content` is a promise read with `use()` in `<Suspense>`
58
+ never holds the page either; only that boundary waits for it.
59
+
60
+ **What this means for effects.** An effect keyed only on the URL
61
+ (`usePathname()`) can run before the destination's values arrive and read the
62
+ previous page's value. That is the contract, not a bug. When a reader must tie a
63
+ handle value to a page (an analytics `page_view`), put the page's path in the
64
+ handle value and key the effect on the value, deduped by path:
65
+
66
+ ```ts
67
+ // handles/page-view.ts
68
+ import { createHandle } from "@rangojs/router";
69
+
70
+ export interface PageView {
71
+ path: string;
72
+ title?: string;
73
+ }
74
+
75
+ // Collect the item, not the array of segment arrays.
76
+ export const PageView = createHandle<PageView, PageView | undefined>(
77
+ (segments) => segments.flat().at(-1),
78
+ );
79
+ ```
80
+
81
+ ```tsx
82
+ // urls: a root slot that re-runs on every navigation
83
+ parallel(
84
+ {
85
+ "@pageView": (ctx) => {
86
+ ctx.use(PageView)({ path: ctx.pathname + ctx.url.search });
87
+ return null;
88
+ },
89
+ },
90
+ () => [revalidate(true)],
91
+ );
92
+ ```
93
+
94
+ ```tsx
95
+ "use client";
96
+ import { useEffect, useRef } from "react";
97
+ import { useHandle } from "@rangojs/router/client";
98
+ import { PageView } from "./handles/page-view";
99
+
100
+ export function PageViews() {
101
+ const pv = useHandle(PageView);
102
+ // Seeded with the landing path: the first page_view is sent elsewhere (a
103
+ // bootstrap script). Start from `null` to send it from here instead. The
104
+ // component also renders on the server, where there is no `window`.
105
+ const last = useRef(
106
+ typeof window === "undefined"
107
+ ? null
108
+ : window.location.pathname + window.location.search,
109
+ );
110
+ useEffect(() => {
111
+ if (!pv || pv.path === last.current) return;
112
+ last.current = pv.path;
113
+ gtag("event", "page_view", { page_path: pv.path, page_title: pv.title });
114
+ }, [pv]);
115
+ return null;
116
+ }
117
+ ```
118
+
119
+ The effect fires once per page, because the value carries its own path: the
120
+ previous page's value has the previous path and is skipped, and the destination's
121
+ fires when it arrives. Trade-offs:
122
+
123
+ - Dedupe is by path. Include `ctx.url.search` in `path` if search-only
124
+ navigations count; a same-URL refresh or an action's revalidation re-delivers
125
+ an equal value and sends nothing. Under dev StrictMode the mount effect runs
126
+ twice; the path check covers it.
127
+ - Select or collect the item, not an array: `useHandle` keeps its reference
128
+ while the value is equal one level deep, and an array of objects is rebuilt on
129
+ every delivery (an action's included). With the default collector, use
130
+ `useHandle(PageView, (s) => s.at(-1)?.at(-1))`.
131
+ - Async values arrive later. With `push(promise)` the page_view fires when the
132
+ promise resolves, with the resolved value; read the title from the value, not
133
+ from `document.title`.
134
+ - A page that pushes nothing under its own `PageView` keeps the previous value
135
+ and fires nothing for itself; the root slot above stamps the path on every
136
+ navigation so every route counts.
137
+ - Back/Forward fires (the entry's path differs from the current one).
138
+
139
+ A server component that pushes after an `await` throws `LateHandlePushError`:
140
+ reserve the slot in the handler with `ctx.use(Handle).defer()` and resolve it
141
+ from the component (`/breadcrumbs`, "Deferred content").
142
+
42
143
  On a document load `useHandle` hydrates with the handle data its HTML was
43
144
  rendered from, wherever the reader sits; late loader pushes reach every reader
44
145
  once the document has finished streaming and every boundary has hydrated (see
@@ -107,6 +208,9 @@ function AddToCartButton({ productId }: { productId: string }) {
107
208
  // payload: unknown[] | FormData | null (the call's arguments)
108
209
  // error: unknown | null (whatever the action threw)
109
210
  // result: unknown | null (return value)
211
+ // revalidationError: unknown | null (a loader that threw while it reran
212
+ // after the action; the action succeeded and the loader threw at its
213
+ // read: /client-urls -> "A loader that fails on its rerun")
110
214
 
111
215
  return (
112
216
  // .bind keeps the form progressively enhanced; useAction still matches it
@@ -35,15 +35,23 @@ on screen:
35
35
 
36
36
  | What you want to show | Read |
37
37
  | ---------------------------------------------- | ----------------------------------------------------------------------------------- |
38
- | a request is out, nothing has arrived | `state === "loading"`; `pendingUrl` is the URL being fetched |
38
+ | a request is out, nothing has arrived | `pendingUrl` is the URL being fetched; `null` once the response arrives |
39
+ | the new page is not on screen yet | `state === "loading"`; `"idle"` commits together with the new page and its URL |
39
40
  | the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
40
41
  | which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
41
42
 
42
- `state` and `pendingUrl` cover the network wait. Once the response has
43
- arrived a loader may still be streaming, and React keeps the page being left
44
- on screen where a navigation holds its content (a same-route change, a
45
- `transition()` route); `isStreaming` covers that part too, so use it for a
46
- progress indicator that should last until the new page is complete.
43
+ `pendingUrl` covers the network wait. Once the response has arrived a loader
44
+ may still be streaming, and React keeps the page being left on screen where a
45
+ navigation holds its content (a same-route change, a `transition()` route,
46
+ with or without a view transition). `state` stays `"loading"` through that
47
+ part: for a navigation on its own, `"idle"` commits in the same commit as the
48
+ destination's `useSearchParams()`, `usePathname()` and `useParams()`, so a
49
+ component that reads both does not see the navigation finished on the URL it
50
+ is leaving. A server action, or another navigation started and aborted,
51
+ while React holds that page can still bring `"idle"` with the URL being left.
52
+ `isStreaming` can outlast that commit (a loader still streaming below a
53
+ boundary that already showed), so use it for a progress indicator that should
54
+ last until the new page is complete.
47
55
 
48
56
  On Back/Forward the address bar changes at once, before any of this: during
49
57
  the wait `window.location` is the entry being fetched, the same URL as
@@ -51,9 +59,9 @@ the wait `window.location` is the entry being fetched, the same URL as
51
59
  from the client's history cache restores its page in one commit and reports
52
60
  no pending state.
53
61
 
54
- `location` moves when the response arrives. Where React then holds the page
55
- being left, it is ahead of the screen for that last part of the wait, and so
56
- is a URL hook in a component that first mounts then (issue #1046).
62
+ `location` moves in that commit too. A component that first mounts while
63
+ React holds the page being left reads the destination's `location` and URL
64
+ hooks (issue #1046).
57
65
 
58
66
  ### useRouter()
59
67
 
@@ -103,7 +111,10 @@ structured-clone-safe data; see [`./state.md`](./state.md)).
103
111
  Target resolution: a path starting with `/` is app-absolute and gets the
104
112
  router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
105
113
  against the current `include()` mount, so `router.push("cart")` inside
106
- `include("/shop", ...)` navigates to `/shop/cart`.
114
+ `include("/shop", ...)` navigates to `/shop/cart`. A parameterized mount is
115
+ filled from the current route params: inside `include("/:accountId", ...)` at
116
+ `/acme/board`, `router.push("cart")` navigates to `/acme/cart`; a missing
117
+ mount param rejects the returned promise.
107
118
 
108
119
  #### Skipping revalidation
109
120
 
@@ -176,6 +187,9 @@ const segments = useSegments();
176
187
  const part = segments.path[depth];
177
188
  ```
178
189
 
190
+ Like `useHandle`, it commits with the page: while React holds the page being
191
+ left, `useSegments()` keeps that page's path and segments.
192
+
179
193
  ### useLinkStatus()
180
194
 
181
195
  Track pending state inside a Link component. `pending` is `true` while a