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

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