@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -863
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1129
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -6,49 +6,85 @@ 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
 
13
14
  ### useNavigation()
14
15
 
15
- Track navigation state and control navigation:
16
+ Track reactive navigation state (state-only, no actions):
16
17
 
17
18
  ```tsx
18
19
  "use client";
19
- import { useNavigation } from "@rangojs/router";
20
+ import { useNavigation } from "@rangojs/router/client";
20
21
 
21
22
  function NavIndicator() {
22
23
  const nav = useNavigation();
23
24
 
24
- // Full state
25
- nav.state; // 'idle' | 'loading' | 'streaming'
26
- nav.isStreaming; // boolean
27
- nav.location; // Current URL
28
- nav.pendingUrl; // Target URL during navigation (or null)
25
+ // State properties
26
+ nav.state; // 'idle' | 'loading'
27
+ nav.isStreaming; // boolean
28
+ nav.location; // Current URL
29
+ nav.pendingUrl; // Target URL during navigation (or null)
29
30
 
30
- // Methods
31
- nav.navigate("/products"); // Navigate programmatically
32
- nav.navigate("/products", { replace: true }); // Replace history
33
- nav.refresh(); // Refresh current route
34
-
35
- return nav.state === 'loading' ? <Spinner /> : null;
31
+ return nav.state === "loading" ? <Spinner /> : null;
36
32
  }
37
33
 
38
- // With selector for performance
34
+ // With selector for performance (re-renders only when selected value changes)
39
35
  function IsLoading() {
40
- const isLoading = useNavigation(nav => nav.state === 'loading');
36
+ const isLoading = useNavigation((nav) => nav.state === "loading");
41
37
  return isLoading ? <Spinner /> : null;
42
38
  }
43
39
  ```
44
40
 
41
+ ### useRouter()
42
+
43
+ Access stable router actions (never causes re-renders):
44
+
45
+ ```tsx
46
+ "use client";
47
+ import { useRouter } from "@rangojs/router/client";
48
+
49
+ function NavigationControls() {
50
+ const router = useRouter();
51
+
52
+ router.push("/products"); // Navigate (adds history entry)
53
+ router.replace("/login"); // Navigate (replaces history entry)
54
+ router.refresh(); // Re-fetch current route data
55
+ router.prefetch("/dashboard"); // Prefetch for faster navigation
56
+ router.back(); // Go back in history
57
+ router.forward(); // Go forward in history
58
+ }
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
+
45
81
  ### useSegments()
46
82
 
47
83
  Access current URL path and matched route segments:
48
84
 
49
85
  ```tsx
50
86
  "use client";
51
- import { useSegments } from "@rangojs/router";
87
+ import { useSegments } from "@rangojs/router/client";
52
88
 
53
89
  function Breadcrumbs() {
54
90
  const { path, segmentIds, location } = useSegments();
@@ -61,7 +97,7 @@ function Breadcrumbs() {
61
97
  }
62
98
 
63
99
  // With selector
64
- const isShopRoute = useSegments(s => s.path[0] === "shop");
100
+ const isShopRoute = useSegments((s) => s.path[0] === "shop");
65
101
  ```
66
102
 
67
103
  ### useLinkStatus()
@@ -81,7 +117,7 @@ function LoadingIndicator() {
81
117
  <Link to="/dashboard">
82
118
  Dashboard
83
119
  <LoadingIndicator />
84
- </Link>
120
+ </Link>;
85
121
  ```
86
122
 
87
123
  ## Data Hooks
@@ -92,7 +128,7 @@ Access loader data (strict - data guaranteed):
92
128
 
93
129
  ```tsx
94
130
  "use client";
95
- import { useLoader } from "@rangojs/router";
131
+ import { useLoader } from "@rangojs/router/client";
96
132
  import { ProductLoader } from "../loaders/product";
97
133
 
98
134
  function ProductPrice() {
@@ -128,7 +164,7 @@ Access loader with on-demand fetching (flexible):
128
164
 
129
165
  ```tsx
130
166
  "use client";
131
- import { useFetchLoader } from "@rangojs/router";
167
+ import { useFetchLoader } from "@rangojs/router/client";
132
168
  import { SearchLoader } from "../loaders/search";
133
169
 
134
170
  function SearchResults() {
@@ -146,37 +182,83 @@ function SearchResults() {
146
182
  <div>
147
183
  <input onChange={(e) => handleSearch(e.target.value)} />
148
184
  {isLoading && <Spinner />}
149
- {data?.results.map(r => <Result key={r.id} {...r} />)}
185
+ {data?.results.map((r) => (
186
+ <Result key={r.id} {...r} />
187
+ ))}
150
188
  </div>
151
189
  );
152
190
  }
153
191
  ```
154
192
 
155
193
  **Load options**:
194
+
156
195
  ```tsx
196
+ // JSON body — sent as application/json, available as ctx.body on the server
157
197
  await load({
158
- method: 'POST', // GET, POST, PUT, PATCH, DELETE
159
- params: { query: 'test' }, // Query string (GET) or body (others)
160
- body: { data: 'value' }, // For POST/PUT/PATCH/DELETE
198
+ method: "POST",
199
+ params: { query: "test" },
200
+ body: { data: "value" },
161
201
  });
202
+
203
+ // FormData body — sent as multipart/form-data, available as ctx.formData on the server.
204
+ // Automatically detected: when body is a FormData instance, the request switches
205
+ // to multipart/form-data to preserve File objects and binary data.
206
+ const formData = new FormData();
207
+ formData.append("file", fileInput.files[0]);
208
+ await load({ method: "POST", body: formData });
162
209
  ```
163
210
 
164
- ### useLoaderData()
211
+ **Body type auto-switching**: The `load()` function inspects the `body` value to
212
+ choose the encoding. If `body instanceof FormData`, the request is sent as
213
+ `multipart/form-data` (browser sets the boundary header automatically). Otherwise
214
+ the body is JSON-serialized and sent with `Content-Type: application/json`. On the
215
+ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.formData`.
165
216
 
166
- Get all loader data in current context:
217
+ **File upload example**:
167
218
 
168
219
  ```tsx
169
220
  "use client";
170
- import { useLoaderData } from "@rangojs/router";
221
+ import { useFetchLoader } from "@rangojs/router/client";
222
+ import { FileUploadLoader } from "../loaders/upload";
223
+
224
+ function FileUploader() {
225
+ const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
226
+ const formRef = useRef<HTMLFormElement>(null);
171
227
 
172
- function DebugPanel() {
173
- const allData = useLoaderData();
174
- // Record<string, any> - Map of loader ID to data
228
+ const handleSubmit = async (formData: FormData) => {
229
+ await load({ method: "POST", body: formData });
230
+ formRef.current?.reset();
231
+ };
175
232
 
176
- return <pre>{JSON.stringify(allData, null, 2)}</pre>;
233
+ return (
234
+ <form ref={formRef} action={handleSubmit}>
235
+ <input type="file" name="file" />
236
+ <button type="submit" disabled={isLoading}>
237
+ {isLoading ? "Uploading..." : "Upload"}
238
+ </button>
239
+ {data?.uploadedFile && <p>Uploaded: {data.uploadedFile.name}</p>}
240
+ </form>
241
+ );
177
242
  }
178
243
  ```
179
244
 
245
+ Server-side loader for the upload:
246
+
247
+ ```typescript
248
+ import { createLoader } from "@rangojs/router";
249
+
250
+ export const FileUploadLoader = createLoader(async (ctx) => {
251
+ "use server";
252
+
253
+ const file = ctx.formData?.get("file") as File | null;
254
+ if (file && file.size > 0) {
255
+ // Process file (save to R2, D1, etc.)
256
+ return { uploadedFile: { name: file.name, size: file.size } };
257
+ }
258
+ return { uploadedFile: null };
259
+ }, true); // true = fetchable (can be called from the client via load())
260
+ ```
261
+
180
262
  ## Handle Hooks
181
263
 
182
264
  ### useHandle()
@@ -185,8 +267,7 @@ Access accumulated handle data from route segments:
185
267
 
186
268
  ```tsx
187
269
  "use client";
188
- import { useHandle } from "@rangojs/router";
189
- import { Breadcrumbs } from "../handles/breadcrumbs";
270
+ import { useHandle, Breadcrumbs } from "@rangojs/router/client";
190
271
 
191
272
  function BreadcrumbNav() {
192
273
  const crumbs = useHandle(Breadcrumbs);
@@ -205,7 +286,7 @@ function BreadcrumbNav() {
205
286
  }
206
287
 
207
288
  // With selector
208
- const lastCrumb = useHandle(Breadcrumbs, data => data.at(-1));
289
+ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
209
290
  ```
210
291
 
211
292
  Handles can be passed as props from server to client components:
@@ -216,16 +297,21 @@ path("/dashboard", (ctx) => {
216
297
  const push = ctx.use(Breadcrumbs);
217
298
  push({ label: "Dashboard", href: "/dashboard" });
218
299
  return <DashboardNav handle={Breadcrumbs} />;
219
- })
300
+ });
220
301
 
221
302
  // Client component — typeof infers the full Handle<T> type
222
- "use client";
223
- import { useHandle } from "@rangojs/router/client";
224
- import type { Breadcrumbs } from "../handles";
303
+ ("use client");
304
+ import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
225
305
 
226
306
  function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
227
307
  const crumbs = useHandle(handle);
228
- return <nav>{crumbs.map(c => <a href={c.href}>{c.label}</a>)}</nav>;
308
+ return (
309
+ <nav>
310
+ {crumbs.map((c) => (
311
+ <a href={c.href}>{c.label}</a>
312
+ ))}
313
+ </nav>
314
+ );
229
315
  }
230
316
  ```
231
317
 
@@ -241,7 +327,7 @@ Track state of server action invocations:
241
327
 
242
328
  ```tsx
243
329
  "use client";
244
- import { useAction } from "@rangojs/router";
330
+ import { useAction } from "@rangojs/router/client";
245
331
  import { addToCart } from "../actions/cart";
246
332
 
247
333
  function AddToCartButton({ productId }: { productId: string }) {
@@ -256,8 +342,8 @@ function AddToCartButton({ productId }: { productId: string }) {
256
342
  return (
257
343
  <form action={addToCart}>
258
344
  <input type="hidden" name="productId" value={productId} />
259
- <button disabled={state === 'loading'}>
260
- {state === 'loading' ? 'Adding...' : 'Add to Cart'}
345
+ <button disabled={state === "loading"}>
346
+ {state === "loading" ? "Adding..." : "Add to Cart"}
261
347
  </button>
262
348
  {error && <p className="error">{error.message}</p>}
263
349
  </form>
@@ -265,7 +351,7 @@ function AddToCartButton({ productId }: { productId: string }) {
265
351
  }
266
352
 
267
353
  // Match by string suffix (convenient but may be ambiguous)
268
- const isLoading = useAction('addToCart', s => s.state === 'loading');
354
+ const isLoading = useAction("addToCart", (s) => s.state === "loading");
269
355
  ```
270
356
 
271
357
  ## State Hooks
@@ -276,20 +362,29 @@ Read type-safe state from history:
276
362
 
277
363
  ```tsx
278
364
  "use client";
279
- import { useLocationState, createLocationState } from "@rangojs/router";
365
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
280
366
 
281
- // Define typed state
367
+ // Define typed state (all export patterns supported)
368
+ // Keys are auto-injected by the Vite plugin -- no manual key needed.
282
369
  export const ProductState = createLocationState<{
283
370
  name: string;
284
371
  price: number;
285
372
  }>();
286
373
 
374
+ // Also valid: const ProductState = createLocationState<...>();
375
+ // export { ProductState };
376
+ // Also valid: export { ProductState as MyState };
377
+
287
378
  function ProductHeader() {
288
379
  const state = useLocationState(ProductState);
289
380
  // { name: string; price: number } | undefined
290
381
 
291
382
  if (state) {
292
- return <h1>{state.name} - ${state.price}</h1>;
383
+ return (
384
+ <h1>
385
+ {state.name} - ${state.price}
386
+ </h1>
387
+ );
293
388
  }
294
389
  return <h1>Loading...</h1>;
295
390
  }
@@ -301,14 +396,114 @@ Pass state through Link:
301
396
  import { Link } from "@rangojs/router/client";
302
397
  import { ProductState } from "./state";
303
398
 
399
+ <Link to="/product/123" state={[ProductState({ name: "Widget", price: 99 })]}>
400
+ View Product
401
+ </Link>;
402
+ ```
403
+
404
+ Pass typed state just in time (getter evaluated at click time, not render time):
405
+
406
+ ```tsx
407
+ "use client"; // JIT state requires a client component (getter can't cross RSC boundary)
408
+
409
+ import { Link } from "@rangojs/router/client";
410
+ import { ProductState } from "./state";
411
+
412
+ // The getter is stored lazily and only called when the user clicks the link.
413
+ // This is useful for capturing values that change after render (e.g., scroll
414
+ // position, form state, ref values).
304
415
  <Link
305
416
  to="/product/123"
306
- state={[ProductState({ name: "Widget", price: 99 })]}
417
+ state={[ProductState(() => ({ name: product.name, price: product.price }))]}
307
418
  >
308
419
  View Product
420
+ </Link>;
421
+ ```
422
+
423
+ Plain state can also be evaluated just in time (also requires a client component):
424
+
425
+ ```tsx
426
+ <Link to="/product/123" state={() => ({ from: window.location.pathname })}>
427
+ View Product
309
428
  </Link>
310
429
  ```
311
430
 
431
+ ### Flash State (read-once)
432
+
433
+ Create a location state with `{ flash: true }` for read-once state that
434
+ auto-clears after first render. Ideal for flash messages (success/error
435
+ notifications after redirect):
436
+
437
+ ```tsx
438
+ // location-states.ts
439
+ import { createLocationState } from "@rangojs/router";
440
+
441
+ export const FlashMessage = createLocationState<{ text: string }>({
442
+ flash: true,
443
+ });
444
+ ```
445
+
446
+ Read flash state with `useLocationState` (same hook as persistent state):
447
+
448
+ ```tsx
449
+ "use client";
450
+ import { useLocationState } from "@rangojs/router/client";
451
+ import { FlashMessage } from "../location-states";
452
+
453
+ function FlashBanner() {
454
+ const flash = useLocationState(FlashMessage);
455
+ // { text: string } | undefined
456
+
457
+ if (!flash) return null;
458
+ return <div className="flash">{flash.text}</div>;
459
+ }
460
+ ```
461
+
462
+ Flash behavior is determined by the definition (`{ flash: true }`), not by which
463
+ hook reads it. `useLocationState` reads the value synchronously during render,
464
+ then clears it from `history.state` via `replaceState` in a `useEffect`.
465
+ Multiple components reading the same flash definition all see the value.
466
+ Pressing back/forward will not re-show the flash since it was cleared.
467
+
468
+ Set flash state from the server via `redirect()` with state:
469
+
470
+ ```tsx
471
+ // In a route handler
472
+ import { redirect, createLocationState } from "@rangojs/router";
473
+
474
+ export const FlashMessage = createLocationState<{ text: string }>({
475
+ flash: true,
476
+ });
477
+
478
+ // Handler
479
+ (ctx) => {
480
+ return redirect("/dashboard", {
481
+ state: [FlashMessage({ text: "Item saved!" })],
482
+ });
483
+ };
484
+ ```
485
+
486
+ Or via `ctx.setLocationState()` on any response:
487
+
488
+ ```tsx
489
+ (ctx) => {
490
+ ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
491
+ return <Dashboard />;
492
+ };
493
+ ```
494
+
495
+ ### .read() (non-hook access)
496
+
497
+ Read current location state outside React components (client-side only):
498
+
499
+ ```tsx
500
+ import { FlashMessage, ProductState } from "../location-states";
501
+
502
+ // Returns TState | undefined. Returns undefined during SSR.
503
+ const flash = FlashMessage.read();
504
+ const product = ProductState.read();
505
+ ```
506
+
312
507
  ## Cache Hooks
313
508
 
314
509
  ### useClientCache()
@@ -317,15 +512,15 @@ Manually control client-side navigation cache:
317
512
 
318
513
  ```tsx
319
514
  "use client";
320
- import { useClientCache } from "@rangojs/router";
515
+ import { useClientCache } from "@rangojs/router/client";
321
516
 
322
517
  function SaveButton() {
323
518
  const { clear } = useClientCache();
324
519
 
325
520
  const handleSave = async () => {
326
- await fetch('/api/data', {
327
- method: 'POST',
328
- body: JSON.stringify(data)
521
+ await fetch("/api/data", {
522
+ method: "POST",
523
+ body: JSON.stringify(data),
329
524
  });
330
525
 
331
526
  // Invalidate cache after mutation
@@ -345,7 +540,7 @@ function SaveButton() {
345
540
  Render child content in layouts:
346
541
 
347
542
  ```tsx
348
- import { Outlet, ParallelOutlet } from "@rangojs/router";
543
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
349
544
 
350
545
  function DashboardLayout({ children }: { children?: React.ReactNode }) {
351
546
  return (
@@ -353,9 +548,7 @@ function DashboardLayout({ children }: { children?: React.ReactNode }) {
353
548
  <aside>
354
549
  <ParallelOutlet name="@sidebar" />
355
550
  </aside>
356
- <main>
357
- {children ?? <Outlet />}
358
- </main>
551
+ <main>{children ?? <Outlet />}</main>
359
552
  <ParallelOutlet name="@notifications" />
360
553
  </div>
361
554
  );
@@ -368,7 +561,7 @@ Access outlet content programmatically:
368
561
 
369
562
  ```tsx
370
563
  "use client";
371
- import { useOutlet } from "@rangojs/router";
564
+ import { useOutlet } from "@rangojs/router/client";
372
565
 
373
566
  function ConditionalLayout() {
374
567
  const outlet = useOutlet();
@@ -384,6 +577,72 @@ function ConditionalLayout() {
384
577
 
385
578
  ## URL Hooks
386
579
 
580
+ ### useParams()
581
+
582
+ Access route params from the current URL:
583
+
584
+ ```tsx
585
+ "use client";
586
+ import { useParams } from "@rangojs/router/client";
587
+
588
+ // Route: /product/:productId
589
+ function ProductPage() {
590
+ const params = useParams();
591
+ // { productId: "123" }
592
+
593
+ return <h1>Product {params.productId}</h1>;
594
+ }
595
+
596
+ // With selector for performance (re-renders only when selected value changes)
597
+ function ProductId() {
598
+ const productId = useParams((p) => p.productId);
599
+ return <span>ID: {productId}</span>;
600
+ }
601
+ ```
602
+
603
+ Returns merged params from all matched route segments. Updates on navigation commit (not during pending navigation).
604
+
605
+ ### usePathname()
606
+
607
+ Access the current URL pathname:
608
+
609
+ ```tsx
610
+ "use client";
611
+ import { usePathname } from "@rangojs/router/client";
612
+
613
+ function CurrentPage() {
614
+ const pathname = usePathname();
615
+ // "/product/123" (no search params)
616
+
617
+ return <span>Current path: {pathname}</span>;
618
+ }
619
+ ```
620
+
621
+ Returns the pathname string without search params or hash. Updates on navigation commit.
622
+
623
+ ### useSearchParams()
624
+
625
+ Access the current URL search params:
626
+
627
+ ```tsx
628
+ "use client";
629
+ import { useSearchParams } from "@rangojs/router/client";
630
+
631
+ function SearchResults() {
632
+ const searchParams = useSearchParams();
633
+ const query = searchParams.get("q"); // "react"
634
+ const page = searchParams.get("page"); // "2"
635
+
636
+ return (
637
+ <div>
638
+ Searching for: {query}, page {page}
639
+ </div>
640
+ );
641
+ }
642
+ ```
643
+
644
+ Returns a `ReadonlyURLSearchParams` (URLSearchParams without mutation methods). During SSR, returns empty params and syncs from the browser URL on mount.
645
+
387
646
  ### useHref()
388
647
 
389
648
  Mount-aware href for client components inside `include()` scopes:
@@ -422,21 +681,24 @@ function MountInfo() {
422
681
  }
423
682
  ```
424
683
 
425
- See `/links` for full URL generation guide including server-side `ctx.href`.
684
+ See `/links` for full URL generation guide including server-side `ctx.reverse`.
426
685
 
427
686
  ## Hook Summary
428
687
 
429
- | Hook | Purpose | Returns |
430
- |------|---------|---------|
431
- | `useHref()` | Mount-aware href | `(path) => string` |
432
- | `useMount()` | Current include() mount path | `string` |
433
- | `useNavigation()` | Navigation state & control | state, navigate, refresh |
434
- | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
435
- | `useLinkStatus()` | Link pending state | { pending } |
436
- | `useLoader()` | Loader data (strict) | data, isLoading, error |
437
- | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
438
- | `useLoaderData()` | All loader data | Record<string, any> |
439
- | `useHandle()` | Accumulated handle data | T (handle type) |
440
- | `useAction()` | Server action state | state, error, result |
441
- | `useLocationState()` | History state | T \| undefined |
442
- | `useClientCache()` | Cache control | { clear } |
688
+ | Hook | Purpose | Returns |
689
+ | -------------------- | --------------------------------- | ----------------------------------------------- |
690
+ | `useParams()` | Route params | `Record<string, string>` or selected value |
691
+ | `usePathname()` | Current pathname | `string` |
692
+ | `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
693
+ | `useHref()` | Mount-aware href | `(path) => string` |
694
+ | `useMount()` | Current include() mount path | `string` |
695
+ | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
696
+ | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
697
+ | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
698
+ | `useLinkStatus()` | Link pending state | { pending } |
699
+ | `useLoader()` | Loader data (strict) | data, isLoading, error |
700
+ | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
701
+ | `useHandle()` | Accumulated handle data | T (handle type) |
702
+ | `useAction()` | Server action state | state, error, result |
703
+ | `useLocationState()` | History state (persists or flash) | T \| undefined |
704
+ | `useClientCache()` | Cache control | { clear } |