@rangojs/router 0.24.4 → 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 (267) 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-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -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
@@ -172,7 +172,7 @@ function ShopNav() {
172
172
  }
173
173
  ```
174
174
 
175
- Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. The function `useHref()` returns is referentially stable within a mount, so it is safe as a hook dependency or memoized prop.
175
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. Under a parameterized mount (`include("/:accountId", ...)`) it fills the mount's params from the current route params (`/acme/cart` at `/acme/board`); a missing one makes the returned function throw `Missing param`. The function is referentially stable while the mount's own param values stay the same, so it is safe as a hook dependency or memoized prop.
176
176
 
177
177
  ### useMount()
178
178
 
@@ -162,7 +162,8 @@ ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unpre
162
162
  `ctx.reverse()` auto-fills missing params from the current request, so the
163
163
  last call returns `/en/c/breads` when the current URL is `/en/...`. That is
164
164
  what you want for in-locale links; to leave the locale, pass it explicitly
165
- (`{ locale: "fr" }`, or `undefined`/`""` for the bare default). Middleware and
165
+ (`{ locale: "fr" }`, or `undefined`/`""` for the bare default; under a
166
+ required `/:locale`, `undefined` throws `Missing param "locale"`). Middleware and
166
167
  response routes are the exception: their `ctx.reverse` fills nothing from the
167
168
  request, so pass `locale` there explicitly.
168
169
 
@@ -92,35 +92,39 @@ Only the first `layout()` in an intercept's `use()` is applied. It is modal
92
92
  chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
93
93
  Attach loaders and the rest to the intercept itself.
94
94
 
95
- ## Intercept Middleware
95
+ ## Middleware and an Intercept
96
96
 
97
- Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
97
+ An intercept has no middleware of its own. The target route's middleware runs
98
+ for the modal on every request that renders it, a `cache()` hit and a
99
+ `Prerender()` target included. The chain for an intercept request is:
98
100
 
99
101
  ```
100
- global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept loaders + handler
102
+ global mw (router.use) -> route mw (the target route's middleware()) -> intercept loaders + handler
101
103
  ```
102
104
 
103
105
  The intercept's loaders are started before its handler is called, so they run
104
- in parallel with it. If intercept middleware returns a `Response` (e.g. a
105
- redirect), the intercept stops there.
106
+ in parallel with it. If route middleware returns a `Response` (e.g. a
107
+ redirect), the request stops there, the modal with it.
106
108
 
107
109
  ```typescript
108
- intercept(
109
- "@modal",
110
- "product",
111
- <ProductModal />,
112
- () => [
113
- middleware(async (ctx, next) => {
114
- // Runs only for this intercept, after global and route middleware
115
- ctx.set("interceptSource", "modal");
116
- await next();
117
- }),
118
- loader(ProductLoader),
119
- ]
120
- )
110
+ path("/product/:id", ProductPage, { name: "product" }, () => [
111
+ middleware(async (ctx, next) => {
112
+ // Runs for the full page and for the modal
113
+ ctx.set(Viewer, await getViewer(ctx.request));
114
+ await next();
115
+ }),
116
+ ]),
117
+ intercept("@modal", "product", <ProductModal />, () => [
118
+ loader(ProductLoader), // ctx.get(Viewer) is set here
119
+ ]),
121
120
  ```
122
121
 
123
- The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
122
+ `middleware()` in an intercept's `use()`, or in its handler's `.use`, throws
123
+ when the routes are defined: move it to the route's `middleware()`, or do the
124
+ work in the intercept's loader.
125
+
126
+ The intercept handler and its loaders read the context variables global and
127
+ route middleware set.
124
128
 
125
129
  Handler/layout `ctx.set()` data follows the same rule as elsewhere:
126
130
  intercepts see data produced in the current render pass, but partial
@@ -136,7 +140,11 @@ re-render every time the intercept resolves live — each intercepted
136
140
  navigation, and each action submitted while the modal is open. When the
137
141
  segment cache or a prerendered intercept variant serves the modal, the stored
138
142
  handler output is reused and only the loaders run. Each loader decides for
139
- itself through its own `revalidate()`, with the normal loader defaults.
143
+ itself through its own `revalidate()`, with the normal loader defaults: an
144
+ action from the modal re-runs only the loaders whose predicate says so, and
145
+ the modal keeps the data of the others, as it does on a link to another
146
+ intercepted route that reads the same loader. In `clientUrls()` the predicate
147
+ runs on the server too (`/client-urls`).
140
148
 
141
149
  A `revalidate()` placed directly in the intercept's `use()` (or returned from
142
150
  its handler's `.use`) throws at definition time. Attach it to the loader:
@@ -144,13 +152,13 @@ its handler's `.use`) throws at definition time. Attach it to the loader:
144
152
  ```typescript
145
153
  // Throws: revalidate() is not valid inside intercept("@modal", "product") use()
146
154
  intercept("@modal", "product", <ProductModal />, () => [
147
- revalidate(() => false),
155
+ revalidate(false),
148
156
  loader(ProductLoader),
149
157
  ]);
150
158
 
151
159
  // Correct: the loader owns the decision
152
160
  intercept("@modal", "product", <ProductModal />, () => [
153
- loader(ProductLoader, () => [revalidate(() => false)]),
161
+ loader(ProductLoader, () => [revalidate(false)]),
154
162
  ]);
155
163
  ```
156
164
 
@@ -176,7 +184,7 @@ layout(ProductLayout, () => [
176
184
  intercept("@modal", "product", <ProductModal />, () => [
177
185
  loader(ProductLoader, () => [
178
186
  revalidate(revalidateProductShell), // reruns with the producer
179
- revalidate(() => false), // otherwise keep the loaded data
187
+ revalidate(false), // otherwise keep the loaded data
180
188
  ]),
181
189
  ]),
182
190
  ]);
@@ -197,7 +205,7 @@ export const revalidateProduct = () => [
197
205
  layout(ProductLayout, () => [
198
206
  revalidateProduct(),
199
207
  intercept("@modal", "product", <ProductModal />, () => [
200
- loader(ProductLoader, () => [revalidateProduct(), revalidate(() => false)]),
208
+ loader(ProductLoader, () => [revalidateProduct(), revalidate(false)]),
201
209
  ]),
202
210
  ]);
203
211
  ```
@@ -244,6 +252,29 @@ back to `Referer`, and only when that `Referer` is on the request's own origin
244
252
  (scheme, host and port). A request with no usable navigation context is not
245
253
  intercepted: it gets the full page.
246
254
 
255
+ ### `prefetch: false` on an intercept's loaders
256
+
257
+ `loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
258
+ of a prefetch of the target from the source page:
259
+
260
+ ```typescript
261
+ intercept("@modal", "product", <ProductModal />, () => [
262
+ loader(ProductLoader),
263
+ loader(ReviewsLoader, { prefetch: false }), // skipped in the prefetch
264
+ loading(<ModalSkeleton />),
265
+ ]);
266
+ ```
267
+
268
+ The click that adopts the prefetch opens the modal at once over the page, with
269
+ its `loading()` while one fill request, sent from the source page, runs the
270
+ flagged loader. The page under the modal is not re-rendered or remounted.
271
+ Without `loading()` the slot shows nothing until the fill answers, as a plain
272
+ click shows nothing until its response. The modal's loaders resolve together,
273
+ so its `loading()` shows until the last one arrives. A handler that reads the
274
+ flagged loader with `ctx.use()` before its first `await` (or one with no
275
+ `loading()`) gets it run in the prefetch instead: one run, no fill for it.
276
+ `loading(…, { prefetch: false })` inside `intercept()` defers nothing.
277
+
247
278
  A prefetch of a route an intercept targets is stored per source page, whether
248
279
  or not the intercept applies from that page (`rsc-rendering.ts`
249
280
  `x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A
@@ -284,7 +315,7 @@ intercept(
284
315
  <ProductModal />,
285
316
  () => [
286
317
  loader(ProductLoader, () => [cache()]),
287
- loader(ProductCartLoader, () => [revalidate(() => true)]),
318
+ loader(ProductCartLoader, () => [revalidate(true)]),
288
319
  loader(RecommendationsLoader),
289
320
  ]
290
321
  )
@@ -295,15 +326,15 @@ intercept(
295
326
  An intercept's `use()` (and its handler's `.use`) accepts:
296
327
 
297
328
  - `loader()`, with its own `revalidate()` and `cache()` inside it
298
- - `middleware()`
299
329
  - `loading()`
300
330
  - `layout(Chrome)`: the modal chrome, without `use()` items of its own
301
331
  - `route`, the item type a `path()` call produces
302
332
  - `transition()`
303
333
 
304
334
  Everything else throws at definition time with a pointer to where it goes:
305
- `revalidate()` onto the loader, `errorBoundary()` / `notFoundBoundary()` onto
306
- the enclosing layout (see below), and `cache()` onto the target route.
335
+ `middleware()` onto the target route, `revalidate()` onto the loader,
336
+ `errorBoundary()` / `notFoundBoundary()` onto the enclosing layout (see
337
+ below), and `cache()` onto the target route.
307
338
 
308
339
  ## Caching an Intercept
309
340
 
@@ -526,7 +557,7 @@ export const shopPatterns = urls(({
526
557
 
527
558
  ## Handler-attached `.use`
528
559
 
529
- Intercept handlers can carry their own middleware, loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
560
+ Intercept handlers can carry their own loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
530
561
 
531
562
  ```typescript
532
563
  import { layout, loader, loading, type Handler } from "@rangojs/router";
@@ -234,7 +234,9 @@ among the path's children (`/caching`).
234
234
  A standalone `layout()` renders when it is first mounted and is then kept as-is:
235
235
 
236
236
  - **Navigation** between routes it already wraps does not re-render it — even
237
- when the URL params change.
237
+ when the URL params change. Links it rendered with `ctx.reverse` (mount
238
+ params auto-filled) keep the old params too; render param-dependent nav in a
239
+ client component with `useReverse(routes)` (`/links`).
238
240
  - **Actions** skip it too (the revalidation trace calls this
239
241
  `action:parent-chain-skip`).
240
242
 
@@ -264,9 +266,10 @@ layout(LocaleLayout, () => [
264
266
  ])
265
267
  ```
266
268
 
267
- `revalidate(() => false)` on a standalone layout only restates the default.
269
+ `revalidate(false)` on a standalone layout only restates the default.
268
270
  The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
269
- are covered in `/loader` → "`revalidate()` return shapes".
271
+ and the `revalidate(true | false)` shorthand are covered in `/loader` →
272
+ "`revalidate()` return shapes".
270
273
 
271
274
  If child segments read data that was established by this layout or by a
272
275
  route handler above them, revalidate the outer segment too. Partial