@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9

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 (293) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +242 -55
  3. package/dist/bin/rango.js +277 -99
  4. package/dist/vite/index.js +2929 -1132
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +68 -21
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +159 -10
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +262 -51
  16. package/skills/host-router/SKILL.md +243 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +291 -31
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +125 -102
  29. package/skills/rango/SKILL.md +242 -23
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +91 -8
  33. package/skills/router-setup/SKILL.md +98 -8
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +511 -188
  37. package/skills/typesafety/SKILL.md +354 -50
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +92 -0
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/link-interceptor.ts +4 -0
  50. package/src/browser/navigation-bridge.ts +183 -18
  51. package/src/browser/navigation-client.ts +187 -57
  52. package/src/browser/navigation-store.ts +75 -17
  53. package/src/browser/navigation-transaction.ts +21 -37
  54. package/src/browser/partial-update.ts +143 -40
  55. package/src/browser/prefetch/cache.ts +275 -28
  56. package/src/browser/prefetch/fetch.ts +191 -46
  57. package/src/browser/prefetch/policy.ts +6 -0
  58. package/src/browser/prefetch/queue.ts +123 -20
  59. package/src/browser/prefetch/resource-ready.ts +77 -0
  60. package/src/browser/rango-state.ts +53 -13
  61. package/src/browser/react/Link.tsx +98 -14
  62. package/src/browser/react/NavigationProvider.tsx +110 -33
  63. package/src/browser/react/context.ts +7 -2
  64. package/src/browser/react/filter-segment-order.ts +51 -7
  65. package/src/browser/react/index.ts +3 -0
  66. package/src/browser/react/location-state-shared.ts +175 -4
  67. package/src/browser/react/location-state.ts +39 -13
  68. package/src/browser/react/use-handle.ts +23 -64
  69. package/src/browser/react/use-navigation.ts +22 -2
  70. package/src/browser/react/use-params.ts +20 -8
  71. package/src/browser/react/use-reverse.ts +106 -0
  72. package/src/browser/react/use-router.ts +43 -10
  73. package/src/browser/react/use-segments.ts +11 -8
  74. package/src/browser/response-adapter.ts +25 -0
  75. package/src/browser/rsc-router.tsx +200 -75
  76. package/src/browser/scroll-restoration.ts +46 -39
  77. package/src/browser/segment-reconciler.ts +36 -9
  78. package/src/browser/segment-structure-assert.ts +2 -2
  79. package/src/browser/server-action-bridge.ts +31 -36
  80. package/src/browser/types.ts +81 -5
  81. package/src/build/collect-fallback-refs.ts +107 -0
  82. package/src/build/generate-manifest.ts +65 -40
  83. package/src/build/generate-route-types.ts +5 -0
  84. package/src/build/index.ts +2 -0
  85. package/src/build/route-trie.ts +69 -26
  86. package/src/build/route-types/codegen.ts +4 -4
  87. package/src/build/route-types/include-resolution.ts +9 -2
  88. package/src/build/route-types/per-module-writer.ts +7 -4
  89. package/src/build/route-types/router-processing.ts +278 -88
  90. package/src/build/route-types/scan-filter.ts +9 -2
  91. package/src/build/route-types/source-scan.ts +118 -0
  92. package/src/build/runtime-discovery.ts +9 -20
  93. package/src/cache/cache-runtime.ts +15 -11
  94. package/src/cache/cache-scope.ts +76 -49
  95. package/src/cache/cf/cf-cache-store.ts +501 -18
  96. package/src/cache/cf/index.ts +5 -1
  97. package/src/cache/document-cache.ts +17 -7
  98. package/src/cache/index.ts +1 -0
  99. package/src/cache/taint.ts +55 -0
  100. package/src/client.rsc.tsx +5 -1
  101. package/src/client.tsx +95 -284
  102. package/src/context-var.ts +72 -2
  103. package/src/debug.ts +2 -2
  104. package/src/decode-loader-results.ts +36 -0
  105. package/src/errors.ts +30 -1
  106. package/src/handle.ts +65 -12
  107. package/src/handles/breadcrumbs.ts +66 -0
  108. package/src/handles/index.ts +1 -0
  109. package/src/host/index.ts +2 -5
  110. package/src/host/router.ts +129 -57
  111. package/src/host/types.ts +31 -2
  112. package/src/host/utils.ts +1 -1
  113. package/src/href-client.ts +140 -20
  114. package/src/index.rsc.ts +15 -40
  115. package/src/index.ts +92 -76
  116. package/src/loader-store.ts +500 -0
  117. package/src/loader.rsc.ts +2 -5
  118. package/src/loader.ts +3 -10
  119. package/src/missing-id-error.ts +68 -0
  120. package/src/outlet-context.ts +1 -1
  121. package/src/prerender/store.ts +57 -15
  122. package/src/prerender.ts +141 -80
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -15
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +435 -260
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +110 -34
  129. package/src/route-definition/index.ts +3 -3
  130. package/src/route-definition/redirect.ts +11 -3
  131. package/src/route-definition/resolve-handler-use.ts +155 -0
  132. package/src/route-definition/use-item-types.ts +32 -0
  133. package/src/route-map-builder.ts +7 -1
  134. package/src/route-types.ts +37 -41
  135. package/src/router/basename.ts +14 -0
  136. package/src/router/content-negotiation.ts +113 -1
  137. package/src/router/error-handling.ts +1 -1
  138. package/src/router/find-match.ts +4 -2
  139. package/src/router/handler-context.ts +105 -39
  140. package/src/router/intercept-resolution.ts +15 -22
  141. package/src/router/lazy-includes.ts +12 -9
  142. package/src/router/loader-resolution.ts +175 -23
  143. package/src/router/logging.ts +5 -2
  144. package/src/router/manifest.ts +31 -16
  145. package/src/router/match-api.ts +129 -193
  146. package/src/router/match-handlers.ts +63 -20
  147. package/src/router/match-middleware/background-revalidation.ts +30 -2
  148. package/src/router/match-middleware/cache-lookup.ts +136 -106
  149. package/src/router/match-middleware/cache-store.ts +54 -10
  150. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  151. package/src/router/match-middleware/segment-resolution.ts +61 -5
  152. package/src/router/match-result.ts +124 -18
  153. package/src/router/metrics.ts +239 -14
  154. package/src/router/middleware-types.ts +61 -31
  155. package/src/router/middleware.ts +226 -124
  156. package/src/router/navigation-snapshot.ts +182 -0
  157. package/src/router/pattern-matching.ts +118 -19
  158. package/src/router/prerender-match.ts +114 -10
  159. package/src/router/preview-match.ts +32 -102
  160. package/src/router/request-classification.ts +286 -0
  161. package/src/router/revalidation.ts +85 -9
  162. package/src/router/route-snapshot.ts +245 -0
  163. package/src/router/router-context.ts +6 -1
  164. package/src/router/router-interfaces.ts +91 -29
  165. package/src/router/router-options.ts +89 -19
  166. package/src/router/router-registry.ts +2 -5
  167. package/src/router/segment-resolution/fresh.ts +240 -23
  168. package/src/router/segment-resolution/helpers.ts +30 -25
  169. package/src/router/segment-resolution/loader-cache.ts +1 -0
  170. package/src/router/segment-resolution/revalidation.ts +483 -289
  171. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  172. package/src/router/segment-wrappers.ts +2 -0
  173. package/src/router/substitute-pattern-params.ts +56 -0
  174. package/src/router/telemetry.ts +99 -0
  175. package/src/router/trie-matching.ts +38 -15
  176. package/src/router/types.ts +9 -0
  177. package/src/router/url-params.ts +49 -0
  178. package/src/router.ts +120 -32
  179. package/src/rsc/handler-context.ts +2 -2
  180. package/src/rsc/handler.ts +524 -370
  181. package/src/rsc/helpers.ts +91 -43
  182. package/src/rsc/index.ts +1 -21
  183. package/src/rsc/loader-fetch.ts +23 -3
  184. package/src/rsc/manifest-init.ts +5 -1
  185. package/src/rsc/origin-guard.ts +28 -10
  186. package/src/rsc/progressive-enhancement.ts +39 -10
  187. package/src/rsc/response-route-handler.ts +46 -53
  188. package/src/rsc/rsc-rendering.ts +69 -89
  189. package/src/rsc/runtime-warnings.ts +9 -10
  190. package/src/rsc/server-action.ts +39 -47
  191. package/src/rsc/ssr-setup.ts +144 -0
  192. package/src/rsc/types.ts +19 -3
  193. package/src/search-params.ts +20 -17
  194. package/src/segment-content-promise.ts +67 -0
  195. package/src/segment-loader-promise.ts +122 -0
  196. package/src/segment-system.tsx +219 -67
  197. package/src/serialize.ts +243 -0
  198. package/src/server/context.ts +285 -63
  199. package/src/server/cookie-store.ts +28 -4
  200. package/src/server/handle-store.ts +19 -0
  201. package/src/server/loader-registry.ts +9 -8
  202. package/src/server/request-context.ts +228 -65
  203. package/src/server.ts +6 -0
  204. package/src/ssr/index.tsx +9 -1
  205. package/src/static-handler.ts +19 -7
  206. package/src/testing/cache-status.ts +166 -0
  207. package/src/testing/collect-handle.ts +63 -0
  208. package/src/testing/dispatch.ts +440 -0
  209. package/src/testing/dom.entry.ts +22 -0
  210. package/src/testing/e2e/fixture.ts +154 -0
  211. package/src/testing/e2e/index.ts +149 -0
  212. package/src/testing/e2e/matchers.ts +51 -0
  213. package/src/testing/e2e/page-helpers.ts +272 -0
  214. package/src/testing/e2e/parity.ts +306 -0
  215. package/src/testing/e2e/server.ts +183 -0
  216. package/src/testing/flight-matchers.ts +104 -0
  217. package/src/testing/flight-runtime.d.ts +21 -0
  218. package/src/testing/flight.entry.ts +22 -0
  219. package/src/testing/flight.ts +182 -0
  220. package/src/testing/generated-routes.ts +223 -0
  221. package/src/testing/index.ts +98 -0
  222. package/src/testing/internal/context.ts +151 -0
  223. package/src/testing/render-route.tsx +536 -0
  224. package/src/testing/run-loader.ts +296 -0
  225. package/src/testing/run-middleware.ts +170 -0
  226. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  227. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  228. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  229. package/src/testing/vitest-stubs/version.ts +5 -0
  230. package/src/testing/vitest.ts +112 -0
  231. package/src/theme/index.ts +4 -13
  232. package/src/types/cache-types.ts +4 -4
  233. package/src/types/global-namespace.ts +39 -26
  234. package/src/types/handler-context.ts +197 -79
  235. package/src/types/index.ts +1 -0
  236. package/src/types/loader-types.ts +41 -15
  237. package/src/types/request-scope.ts +126 -0
  238. package/src/types/route-config.ts +17 -8
  239. package/src/types/route-entry.ts +19 -1
  240. package/src/types/segments.ts +37 -6
  241. package/src/urls/include-helper.ts +34 -67
  242. package/src/urls/index.ts +0 -3
  243. package/src/urls/path-helper-types.ts +50 -9
  244. package/src/urls/path-helper.ts +63 -63
  245. package/src/urls/pattern-types.ts +48 -19
  246. package/src/urls/response-types.ts +25 -22
  247. package/src/urls/type-extraction.ts +26 -116
  248. package/src/urls/urls-function.ts +1 -5
  249. package/src/use-loader.tsx +487 -44
  250. package/src/vite/debug.ts +185 -0
  251. package/src/vite/discovery/bundle-postprocess.ts +63 -91
  252. package/src/vite/discovery/discover-routers.ts +106 -53
  253. package/src/vite/discovery/discovery-errors.ts +194 -0
  254. package/src/vite/discovery/gate-state.ts +171 -0
  255. package/src/vite/discovery/prerender-collection.ts +222 -107
  256. package/src/vite/discovery/route-types-writer.ts +40 -84
  257. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  258. package/src/vite/discovery/state.ts +50 -13
  259. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  260. package/src/vite/index.ts +10 -3
  261. package/src/vite/plugin-types.ts +111 -72
  262. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  263. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  265. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  266. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  267. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  268. package/src/vite/plugins/expose-action-id.ts +55 -33
  269. package/src/vite/plugins/expose-id-utils.ts +24 -8
  270. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  271. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  272. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  273. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  274. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  275. package/src/vite/plugins/performance-tracks.ts +92 -0
  276. package/src/vite/plugins/refresh-cmd.ts +127 -0
  277. package/src/vite/plugins/use-cache-transform.ts +65 -50
  278. package/src/vite/plugins/version-injector.ts +39 -23
  279. package/src/vite/plugins/version-plugin.ts +72 -3
  280. package/src/vite/plugins/virtual-entries.ts +2 -2
  281. package/src/vite/rango.ts +265 -226
  282. package/src/vite/router-discovery.ts +924 -137
  283. package/src/vite/utils/ast-handler-extract.ts +15 -15
  284. package/src/vite/utils/banner.ts +4 -4
  285. package/src/vite/utils/bundle-analysis.ts +4 -2
  286. package/src/vite/utils/client-chunks.ts +190 -0
  287. package/src/vite/utils/forward-user-plugins.ts +193 -0
  288. package/src/vite/utils/manifest-utils.ts +21 -5
  289. package/src/vite/utils/package-resolution.ts +41 -1
  290. package/src/vite/utils/prerender-utils.ts +98 -5
  291. package/src/vite/utils/shared-utils.ts +109 -27
  292. package/src/browser/action-response-classifier.ts +0 -99
  293. package/src/route-definition/route-function.ts +0 -119
@@ -6,7 +6,8 @@ argument-hint: [hook-name]
6
6
 
7
7
  # Client-Side React Hooks
8
8
 
9
- All hooks are imported from `@rangojs/router` or `@rangojs/router/client`.
9
+ Import the hooks and components in this skill from `@rangojs/router/client`.
10
+ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
10
11
 
11
12
  ## Navigation Hooks
12
13
 
@@ -57,13 +58,33 @@ function NavigationControls() {
57
58
  }
58
59
  ```
59
60
 
61
+ #### Skipping revalidation
62
+
63
+ Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
64
+
65
+ ```tsx
66
+ // Update search params without server round-trip
67
+ router.push("/products?color=blue", { revalidate: false });
68
+ router.replace("/products?page=3", { revalidate: false });
69
+ ```
70
+
71
+ If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
72
+
73
+ ```tsx
74
+ <Link to="/products?color=blue" revalidate={false}>
75
+ Blue
76
+ </Link>
77
+ ```
78
+
79
+ Plain `<a>` tags can opt in via `data-revalidate="false"`.
80
+
60
81
  ### useSegments()
61
82
 
62
83
  Access current URL path and matched route segments:
63
84
 
64
85
  ```tsx
65
86
  "use client";
66
- import { useSegments } from "@rangojs/router";
87
+ import { useSegments } from "@rangojs/router/client";
67
88
 
68
89
  function Breadcrumbs() {
69
90
  const { path, segmentIds, location } = useSegments();
@@ -107,7 +128,7 @@ Access loader data (strict - data guaranteed):
107
128
 
108
129
  ```tsx
109
130
  "use client";
110
- import { useLoader } from "@rangojs/router";
131
+ import { useLoader } from "@rangojs/router/client";
111
132
  import { ProductLoader } from "../loaders/product";
112
133
 
113
134
  function ProductPrice() {
@@ -143,7 +164,7 @@ Access loader with on-demand fetching (flexible):
143
164
 
144
165
  ```tsx
145
166
  "use client";
146
- import { useFetchLoader } from "@rangojs/router";
167
+ import { useFetchLoader } from "@rangojs/router/client";
147
168
  import { SearchLoader } from "../loaders/search";
148
169
 
149
170
  function SearchResults() {
@@ -169,6 +190,141 @@ function SearchResults() {
169
190
  }
170
191
  ```
171
192
 
193
+ **Shared refetch behavior**:
194
+
195
+ When the loader is registered on the route via `loader()`, a plain
196
+ `load()` call (no options, or a trivially-defaulted GET with no
197
+ `params` and no `body`) broadcasts its result to every component
198
+ reading the same loader id. Layout, page, and parallel-slot reads
199
+ all converge on the new value:
200
+
201
+ ```tsx
202
+ // Layout button calls load() — the page read below sees the update too.
203
+ function Layout() {
204
+ const { data, load } = useLoader(CartLoader);
205
+ return <button onClick={() => load()}>Refresh ({data.count})</button>;
206
+ }
207
+ function Page() {
208
+ const { data } = useLoader(CartLoader); // updates with the layout's load()
209
+ return <span>{data.count} items</span>;
210
+ }
211
+ ```
212
+
213
+ `isLoading` and `error` follow the same scope. `throwOnError: true`
214
+ render-throws are scoped to the **originating** hook — sibling readers
215
+ see the error in their `error` state but their boundaries are not
216
+ triggered by someone else's failure. A successful follow-up `load()`
217
+ clears the shared error.
218
+
219
+ **`load()` calls that stay local** (no broadcast, per-hook state, same
220
+ semantics as the old per-component `useState`):
221
+
222
+ - `load({ params: { ... } })` — explicit params.
223
+ - `load({ method: "POST", body })` — mutations.
224
+ - Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
225
+ registered on the current route. Two unrelated components calling
226
+ `load()` on the same fetchable-but-unregistered loader keep
227
+ independent results.
228
+
229
+ So the search/list pattern still works — two components calling
230
+ `load({ params: { q } })` with different `q` values each keep their
231
+ own result; they do not collapse to last-write-wins through a shared
232
+ store.
233
+
234
+ **Scoping refetch with a `key`**:
235
+
236
+ Pass a `key` to partition the shared refresh store. Only hooks using the
237
+ **same** `key` refresh together when one of them calls `load()`. This is a
238
+ client-side refresh identity only — it never changes the request sent to the
239
+ server, and is unrelated to the server `cache({ key })` option and to
240
+ `revalidate()`.
241
+
242
+ ```tsx
243
+ // Two independent dashboards using the same loader. Without a key, one
244
+ // dashboard's load() would flip the other's spinner and value. With a key,
245
+ // they refresh independently.
246
+ function Dashboard({ id }: { id: string }) {
247
+ const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
248
+ return <button onClick={() => load()}>Refresh {data.total}</button>;
249
+ }
250
+ ```
251
+
252
+ The `key` widens sharing in two ways the default cannot:
253
+
254
+ - **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
255
+ with the same `q` in two components share one result and refresh together —
256
+ a keyed `load({ params: { q } })` broadcasts to the group instead of staying
257
+ local. (Mutations — non-GET or `body` — stay local even with a key.)
258
+ - **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
259
+ that is **not** registered on the route share too, letting unrelated
260
+ components opt into a common refresh group.
261
+
262
+ Lifecycle: a keyed read of an unregistered loader is reference-counted — its
263
+ shared value lives as long as at least one component using that key is mounted.
264
+ A persistent component (e.g. a header) keeps the value across navigations; a
265
+ route-scoped component's value is reclaimed when it unmounts. Registered-loader
266
+ reads (keyed or not) reset on navigation from fresh route data, as before.
267
+
268
+ **Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
269
+
270
+ `key` groups readers of one loader. To refresh **different** loaders together,
271
+ tag them with a shared `refreshGroup` name and trigger them with
272
+ `useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
273
+ function it returns, so one `useRefreshLoaders()` can refresh different groups
274
+ depending on context. A read may carry **several** tags — pass an array — and is
275
+ refreshed when **any** of its groups is refreshed:
276
+
277
+ ```tsx
278
+ function Profile() {
279
+ const { data } = useLoader(ProfileLoader, {
280
+ key: userId,
281
+ refreshGroup: "account",
282
+ });
283
+ return <span>{data.name}</span>;
284
+ }
285
+ function Orders() {
286
+ // Tagged into two groups: refreshed by "account" (the whole set) or the
287
+ // finer "orders" tag.
288
+ const { data } = useLoader(OrdersLoader, {
289
+ key: userId,
290
+ refreshGroup: ["account", "orders"],
291
+ });
292
+ return <span>{data.count} orders</span>;
293
+ }
294
+ function RefreshButtons() {
295
+ const refresh = useRefreshLoaders();
296
+ return (
297
+ <>
298
+ <button onClick={() => refresh("account")}>Refresh account</button>
299
+ <button onClick={() => refresh("orders")}>Refresh orders only</button>
300
+ <button onClick={() => refresh(["account", "orders"])}>
301
+ Refresh both
302
+ </button>
303
+ </>
304
+ );
305
+ }
306
+ ```
307
+
308
+ `refresh(groups)` accepts one name or an array and re-runs every currently-mounted
309
+ member tagged with **any** of them, with a **plain GET** against the current route
310
+ URL — no params, no body, no mutation methods, because a group spans loaders with
311
+ different shapes. A member that sits in two of the requested groups is fetched
312
+ once (members are unioned and deduped by read). It returns a promise that resolves
313
+ when all members settle and **rejects with an `AggregateError`** if any fail;
314
+ group refresh never render-throws, so handle failures at the await site
315
+ (`await refresh("account").catch(...)`). Each failing member also exposes its
316
+ error via its own read's `error`.
317
+
318
+ Multiple tags give you granular vs. whole-set refresh from one place: a coarse
319
+ tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
320
+ subset. Sharing within a group is opt-in via `key`: members that share a `key`
321
+ share one value (and one fetch); a grouped reader **without** a `key` gets its own
322
+ private bucket, so a group refresh updates only that read and never leaks into
323
+ unrelated unkeyed reads of the same loader. A bucket may belong to several groups
324
+ at once (one read tagged with multiple names, or different reads tagging the same
325
+ keyed bucket with different names). Keep parameterized loaders on the single-loader
326
+ `key` — a plain-GET group refresh sends no params.
327
+
172
328
  **Load options**:
173
329
 
174
330
  ```tsx
@@ -197,7 +353,7 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
197
353
 
198
354
  ```tsx
199
355
  "use client";
200
- import { useFetchLoader } from "@rangojs/router";
356
+ import { useFetchLoader } from "@rangojs/router/client";
201
357
  import { FileUploadLoader } from "../loaders/upload";
202
358
 
203
359
  function FileUploader() {
@@ -238,22 +394,6 @@ export const FileUploadLoader = createLoader(async (ctx) => {
238
394
  }, true); // true = fetchable (can be called from the client via load())
239
395
  ```
240
396
 
241
- ### useLoaderData()
242
-
243
- Get all loader data in current context:
244
-
245
- ```tsx
246
- "use client";
247
- import { useLoaderData } from "@rangojs/router";
248
-
249
- function DebugPanel() {
250
- const allData = useLoaderData();
251
- // Record<string, any> - Map of loader ID to data
252
-
253
- return <pre>{JSON.stringify(allData, null, 2)}</pre>;
254
- }
255
- ```
256
-
257
397
  ## Handle Hooks
258
398
 
259
399
  ### useHandle()
@@ -262,8 +402,7 @@ Access accumulated handle data from route segments:
262
402
 
263
403
  ```tsx
264
404
  "use client";
265
- import { useHandle } from "@rangojs/router";
266
- import { Breadcrumbs } from "../handles/breadcrumbs";
405
+ import { useHandle, Breadcrumbs } from "@rangojs/router/client";
267
406
 
268
407
  function BreadcrumbNav() {
269
408
  const crumbs = useHandle(Breadcrumbs);
@@ -294,11 +433,12 @@ path("/dashboard", (ctx) => {
294
433
  push({ label: "Dashboard", href: "/dashboard" });
295
434
  return <DashboardNav handle={Breadcrumbs} />;
296
435
  });
436
+ ```
297
437
 
438
+ ```tsx
298
439
  // Client component — typeof infers the full Handle<T> type
299
- ("use client");
300
- import { useHandle } from "@rangojs/router/client";
301
- import type { Breadcrumbs } from "../handles";
440
+ "use client";
441
+ import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
302
442
 
303
443
  function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
304
444
  const crumbs = useHandle(handle);
@@ -318,13 +458,18 @@ RSC serialization strips the `collect` function via `toJSON()`. On the client,
318
458
 
319
459
  ## Action Hooks
320
460
 
461
+ For the full server-action guide (defining actions, `useActionState`,
462
+ `useOptimistic`, validation, revalidation, error handling, file uploads),
463
+ see `/server-actions`. `useAction()` below is a Rango-specific hook for
464
+ tracking actions called outside a `<form action={...}>` flow.
465
+
321
466
  ### useAction()
322
467
 
323
468
  Track state of server action invocations:
324
469
 
325
470
  ```tsx
326
471
  "use client";
327
- import { useAction } from "@rangojs/router";
472
+ import { useAction } from "@rangojs/router/client";
328
473
  import { addToCart } from "../actions/cart";
329
474
 
330
475
  function AddToCartButton({ productId }: { productId: string }) {
@@ -359,7 +504,7 @@ Read type-safe state from history:
359
504
 
360
505
  ```tsx
361
506
  "use client";
362
- import { useLocationState, createLocationState } from "@rangojs/router";
507
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
363
508
 
364
509
  // Define typed state (all export patterns supported)
365
510
  // Keys are auto-injected by the Vite plugin -- no manual key needed.
@@ -501,6 +646,43 @@ const flash = FlashMessage.read();
501
646
  const product = ProductState.read();
502
647
  ```
503
648
 
649
+ > **Hydration:** `.read()` returns `undefined` on the server but may return
650
+ > a real value on the first client render (history state survives reload).
651
+ > Do not call `.read()` directly during the initial render of a component;
652
+ > call it from an event handler or inside a `useEffect` post-mount. For
653
+ > reactive hydration-safe access, use `useLocationState()` instead.
654
+
655
+ ### .write() / .delete() (static, non-reactive)
656
+
657
+ Static counterparts to `.read()`. Both mutate the current history entry's
658
+ `history.state` via `replaceState`, preserving any other keys (router
659
+ bookkeeping, other location state slots). Both are client-only; they throw
660
+ when called on the server.
661
+
662
+ Neither dispatches an event, so components reading via `useLocationState`
663
+ will NOT re-render until the next navigation/popstate. Pair with `.read()`
664
+ (or a fresh mount via back/forward/reload) instead.
665
+
666
+ ```tsx
667
+ "use client";
668
+ import { ProductState } from "./state";
669
+
670
+ // Persisted across hard refresh and back/forward of this entry.
671
+ ProductState.write({ name: "Widget", price: 9.99 });
672
+
673
+ // Read later (or on next mount).
674
+ const current = ProductState.read();
675
+
676
+ // Manually clear the slot. Idempotent if it isn't set.
677
+ ProductState.delete();
678
+ ```
679
+
680
+ | Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
681
+ | ----------- | ----------------------- | --------------------------------- | ------------------- |
682
+ | `.read()` | no | n/a (returns snapshot) | returns `undefined` |
683
+ | `.write()` | yes (replace this slot) | no | throws |
684
+ | `.delete()` | yes (remove this slot) | no | throws |
685
+
504
686
  ## Cache Hooks
505
687
 
506
688
  ### useClientCache()
@@ -509,7 +691,7 @@ Manually control client-side navigation cache:
509
691
 
510
692
  ```tsx
511
693
  "use client";
512
- import { useClientCache } from "@rangojs/router";
694
+ import { useClientCache } from "@rangojs/router/client";
513
695
 
514
696
  function SaveButton() {
515
697
  const { clear } = useClientCache();
@@ -537,7 +719,7 @@ function SaveButton() {
537
719
  Render child content in layouts:
538
720
 
539
721
  ```tsx
540
- import { Outlet, ParallelOutlet } from "@rangojs/router";
722
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
541
723
 
542
724
  function DashboardLayout({ children }: { children?: React.ReactNode }) {
543
725
  return (
@@ -558,7 +740,7 @@ Access outlet content programmatically:
558
740
 
559
741
  ```tsx
560
742
  "use client";
561
- import { useOutlet } from "@rangojs/router";
743
+ import { useOutlet } from "@rangojs/router/client";
562
744
 
563
745
  function ConditionalLayout() {
564
746
  const outlet = useOutlet();
@@ -590,6 +772,12 @@ function ProductPage() {
590
772
  return <h1>Product {params.productId}</h1>;
591
773
  }
592
774
 
775
+ // Annotate the expected shape via a generic
776
+ function ProductPageTyped() {
777
+ const { productId } = useParams<{ productId: string }>();
778
+ return <h1>Product {productId}</h1>;
779
+ }
780
+
593
781
  // With selector for performance (re-renders only when selected value changes)
594
782
  function ProductId() {
595
783
  const productId = useParams((p) => p.productId);
@@ -597,7 +785,7 @@ function ProductId() {
597
785
  }
598
786
  ```
599
787
 
600
- Returns merged params from all matched route segments. Updates on navigation commit (not during pending navigation).
788
+ Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
601
789
 
602
790
  ### usePathname()
603
791
 
@@ -678,25 +866,48 @@ function MountInfo() {
678
866
  }
679
867
  ```
680
868
 
681
- See `/links` for full URL generation guide including server-side `ctx.reverse`.
869
+ ### useReverse(routes)
870
+
871
+ Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
872
+
873
+ > Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
874
+
875
+ ```tsx
876
+ "use client";
877
+ import { Link, useReverse } from "@rangojs/router/client";
878
+ import { routes as blogRoutes } from "../urls/blog.gen.js";
879
+
880
+ function BlogNav() {
881
+ const reverse = useReverse(blogRoutes);
882
+ return (
883
+ <nav>
884
+ <Link to={reverse("index")}>Blog</Link>
885
+ <Link to={reverse("post", { postId: "hello" })}>Post</Link>
886
+ </nav>
887
+ );
888
+ }
889
+ ```
890
+
891
+ See `/links` for the full URL generation guide. `ctx.reverse()` is server-only; on the client, prefer `useReverse(routes)` for in-module names and pass URLs as props for cross-module ones.
682
892
 
683
893
  ## Hook Summary
684
894
 
685
- | Hook | Purpose | Returns |
686
- | -------------------- | --------------------------------- | ----------------------------------------------- |
687
- | `useParams()` | Route params | `Record<string, string>` or selected value |
688
- | `usePathname()` | Current pathname | `string` |
689
- | `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
690
- | `useHref()` | Mount-aware href | `(path) => string` |
691
- | `useMount()` | Current include() mount path | `string` |
692
- | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
693
- | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
694
- | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
695
- | `useLinkStatus()` | Link pending state | { pending } |
696
- | `useLoader()` | Loader data (strict) | data, isLoading, error |
697
- | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
698
- | `useLoaderData()` | All loader data | Record<string, any> |
699
- | `useHandle()` | Accumulated handle data | T (handle type) |
700
- | `useAction()` | Server action state | state, error, result |
701
- | `useLocationState()` | History state (persists or flash) | T \| undefined |
702
- | `useClientCache()` | Cache control | { clear } |
895
+ | Hook | Purpose | Returns |
896
+ | --------------------- | --------------------------------- | ------------------------------------------------------------------ |
897
+ | `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
898
+ | `usePathname()` | Current pathname | `string` |
899
+ | `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
900
+ | `useHref()` | Mount-aware href | `(path) => string` |
901
+ | `useMount()` | Current include() mount path | `string` |
902
+ | `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
903
+ | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
904
+ | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
905
+ | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
906
+ | `useLinkStatus()` | Link pending state | { pending } |
907
+ | `useLoader()` | Loader data (strict) | data, isLoading, error |
908
+ | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
909
+ | `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
910
+ | `useHandle()` | Accumulated handle data | T (handle type) |
911
+ | `useAction()` | Server action state | state, error, result |
912
+ | `useLocationState()` | History state (persists or flash) | T \| undefined |
913
+ | `useClientCache()` | Cache control | { clear } |