@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.31

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