@rangojs/router 0.0.0-experimental.002d056c

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 (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  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 +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,704 @@
1
+ ---
2
+ name: hooks
3
+ description: Client-side React hooks for navigation, loaders, and state in @rangojs/router
4
+ argument-hint: [hook-name]
5
+ ---
6
+
7
+ # Client-Side React Hooks
8
+
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.
11
+
12
+ ## Navigation Hooks
13
+
14
+ ### useNavigation()
15
+
16
+ Track reactive navigation state (state-only, no actions):
17
+
18
+ ```tsx
19
+ "use client";
20
+ import { useNavigation } from "@rangojs/router/client";
21
+
22
+ function NavIndicator() {
23
+ const nav = useNavigation();
24
+
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)
30
+
31
+ return nav.state === "loading" ? <Spinner /> : null;
32
+ }
33
+
34
+ // With selector for performance (re-renders only when selected value changes)
35
+ function IsLoading() {
36
+ const isLoading = useNavigation((nav) => nav.state === "loading");
37
+ return isLoading ? <Spinner /> : null;
38
+ }
39
+ ```
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
+
81
+ ### useSegments()
82
+
83
+ Access current URL path and matched route segments:
84
+
85
+ ```tsx
86
+ "use client";
87
+ import { useSegments } from "@rangojs/router/client";
88
+
89
+ function Breadcrumbs() {
90
+ const { path, segmentIds, location } = useSegments();
91
+
92
+ // path: ["/shop", "products", "123"]
93
+ // segmentIds: ["shop-layout", "products-route"]
94
+ // location: URL object
95
+
96
+ return <nav>{path.join(" > ")}</nav>;
97
+ }
98
+
99
+ // With selector
100
+ const isShopRoute = useSegments((s) => s.path[0] === "shop");
101
+ ```
102
+
103
+ ### useLinkStatus()
104
+
105
+ Track pending state inside a Link component:
106
+
107
+ ```tsx
108
+ "use client";
109
+ import { Link, useLinkStatus } from "@rangojs/router/client";
110
+
111
+ function LoadingIndicator() {
112
+ const { pending } = useLinkStatus();
113
+ return pending ? <Spinner /> : null;
114
+ }
115
+
116
+ // Must be inside Link
117
+ <Link to="/dashboard">
118
+ Dashboard
119
+ <LoadingIndicator />
120
+ </Link>;
121
+ ```
122
+
123
+ ## Data Hooks
124
+
125
+ ### useLoader()
126
+
127
+ Access loader data (strict - data guaranteed):
128
+
129
+ ```tsx
130
+ "use client";
131
+ import { useLoader } from "@rangojs/router/client";
132
+ import { ProductLoader } from "../loaders/product";
133
+
134
+ function ProductPrice() {
135
+ const { data, isLoading, error } = useLoader(ProductLoader);
136
+
137
+ // data: T (guaranteed - throws if not in context)
138
+ // isLoading: boolean
139
+ // error: Error | null
140
+
141
+ return <span>${data.price}</span>;
142
+ }
143
+ ```
144
+
145
+ **Precondition**: Loader must be registered on route via `loader()` helper.
146
+
147
+ Loaders can also be passed as props from server to client components:
148
+
149
+ ```tsx
150
+ "use client";
151
+ import { useLoader } from "@rangojs/router/client";
152
+ import type { ProductLoader } from "../loaders";
153
+
154
+ // typeof infers the full data type from the loader definition
155
+ function ProductCard({ loader }: { loader: typeof ProductLoader }) {
156
+ const { data } = useLoader(loader);
157
+ return <h2>{data.product.name}</h2>;
158
+ }
159
+ ```
160
+
161
+ ### useFetchLoader()
162
+
163
+ Access loader with on-demand fetching (flexible):
164
+
165
+ ```tsx
166
+ "use client";
167
+ import { useFetchLoader } from "@rangojs/router/client";
168
+ import { SearchLoader } from "../loaders/search";
169
+
170
+ function SearchResults() {
171
+ const { data, load, isLoading, error } = useFetchLoader(SearchLoader);
172
+
173
+ // data: T | undefined (may be undefined if not fetched)
174
+ // load: (options?) => Promise<T>
175
+ // refetch: alias for load
176
+
177
+ const handleSearch = async (query: string) => {
178
+ await load({ params: { query } });
179
+ };
180
+
181
+ return (
182
+ <div>
183
+ <input onChange={(e) => handleSearch(e.target.value)} />
184
+ {isLoading && <Spinner />}
185
+ {data?.results.map((r) => (
186
+ <Result key={r.id} {...r} />
187
+ ))}
188
+ </div>
189
+ );
190
+ }
191
+ ```
192
+
193
+ **Load options**:
194
+
195
+ ```tsx
196
+ // JSON body — sent as application/json, available as ctx.body on the server
197
+ await load({
198
+ method: "POST",
199
+ params: { query: "test" },
200
+ body: { data: "value" },
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 });
209
+ ```
210
+
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`.
216
+
217
+ **File upload example**:
218
+
219
+ ```tsx
220
+ "use client";
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);
227
+
228
+ const handleSubmit = async (formData: FormData) => {
229
+ await load({ method: "POST", body: formData });
230
+ formRef.current?.reset();
231
+ };
232
+
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
+ );
242
+ }
243
+ ```
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
+
262
+ ## Handle Hooks
263
+
264
+ ### useHandle()
265
+
266
+ Access accumulated handle data from route segments:
267
+
268
+ ```tsx
269
+ "use client";
270
+ import { useHandle, Breadcrumbs } from "@rangojs/router/client";
271
+
272
+ function BreadcrumbNav() {
273
+ const crumbs = useHandle(Breadcrumbs);
274
+ // Array of { label, href } accumulated from layouts/routes
275
+
276
+ return (
277
+ <nav>
278
+ {crumbs.map((c, i) => (
279
+ <span key={i}>
280
+ <a href={c.href}>{c.label}</a>
281
+ {i < crumbs.length - 1 && " > "}
282
+ </span>
283
+ ))}
284
+ </nav>
285
+ );
286
+ }
287
+
288
+ // With selector
289
+ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
290
+ ```
291
+
292
+ Handles can be passed as props from server to client components:
293
+
294
+ ```tsx
295
+ // Server component
296
+ path("/dashboard", (ctx) => {
297
+ const push = ctx.use(Breadcrumbs);
298
+ push({ label: "Dashboard", href: "/dashboard" });
299
+ return <DashboardNav handle={Breadcrumbs} />;
300
+ });
301
+
302
+ // Client component — typeof infers the full Handle<T> type
303
+ ("use client");
304
+ import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
305
+
306
+ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
307
+ const crumbs = useHandle(handle);
308
+ return (
309
+ <nav>
310
+ {crumbs.map((c) => (
311
+ <a href={c.href}>{c.label}</a>
312
+ ))}
313
+ </nav>
314
+ );
315
+ }
316
+ ```
317
+
318
+ RSC serialization strips the `collect` function via `toJSON()`. On the client,
319
+ `useHandle()` recovers it from the module-level registry (populated when
320
+ `createHandle()` runs during module initialization).
321
+
322
+ ## Action Hooks
323
+
324
+ ### useAction()
325
+
326
+ Track state of server action invocations:
327
+
328
+ ```tsx
329
+ "use client";
330
+ import { useAction } from "@rangojs/router/client";
331
+ import { addToCart } from "../actions/cart";
332
+
333
+ function AddToCartButton({ productId }: { productId: string }) {
334
+ const { state, error, result } = useAction(addToCart);
335
+
336
+ // state: 'idle' | 'loading' | 'streaming'
337
+ // actionId: string | null
338
+ // payload: unknown | null (input data)
339
+ // error: Error | null
340
+ // result: unknown | null (return value)
341
+
342
+ return (
343
+ <form action={addToCart}>
344
+ <input type="hidden" name="productId" value={productId} />
345
+ <button disabled={state === "loading"}>
346
+ {state === "loading" ? "Adding..." : "Add to Cart"}
347
+ </button>
348
+ {error && <p className="error">{error.message}</p>}
349
+ </form>
350
+ );
351
+ }
352
+
353
+ // Match by string suffix (convenient but may be ambiguous)
354
+ const isLoading = useAction("addToCart", (s) => s.state === "loading");
355
+ ```
356
+
357
+ ## State Hooks
358
+
359
+ ### useLocationState()
360
+
361
+ Read type-safe state from history:
362
+
363
+ ```tsx
364
+ "use client";
365
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
366
+
367
+ // Define typed state (all export patterns supported)
368
+ // Keys are auto-injected by the Vite plugin -- no manual key needed.
369
+ export const ProductState = createLocationState<{
370
+ name: string;
371
+ price: number;
372
+ }>();
373
+
374
+ // Also valid: const ProductState = createLocationState<...>();
375
+ // export { ProductState };
376
+ // Also valid: export { ProductState as MyState };
377
+
378
+ function ProductHeader() {
379
+ const state = useLocationState(ProductState);
380
+ // { name: string; price: number } | undefined
381
+
382
+ if (state) {
383
+ return (
384
+ <h1>
385
+ {state.name} - ${state.price}
386
+ </h1>
387
+ );
388
+ }
389
+ return <h1>Loading...</h1>;
390
+ }
391
+ ```
392
+
393
+ Pass state through Link:
394
+
395
+ ```tsx
396
+ import { Link } from "@rangojs/router/client";
397
+ import { ProductState } from "./state";
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).
415
+ <Link
416
+ to="/product/123"
417
+ state={[ProductState(() => ({ name: product.name, price: product.price }))]}
418
+ >
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
428
+ </Link>
429
+ ```
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
+
507
+ ## Cache Hooks
508
+
509
+ ### useClientCache()
510
+
511
+ Manually control client-side navigation cache:
512
+
513
+ ```tsx
514
+ "use client";
515
+ import { useClientCache } from "@rangojs/router/client";
516
+
517
+ function SaveButton() {
518
+ const { clear } = useClientCache();
519
+
520
+ const handleSave = async () => {
521
+ await fetch("/api/data", {
522
+ method: "POST",
523
+ body: JSON.stringify(data),
524
+ });
525
+
526
+ // Invalidate cache after mutation
527
+ clear();
528
+ };
529
+
530
+ return <button onClick={handleSave}>Save</button>;
531
+ }
532
+ ```
533
+
534
+ **Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
535
+
536
+ ## Outlet Components
537
+
538
+ ### Outlet / ParallelOutlet
539
+
540
+ Render child content in layouts:
541
+
542
+ ```tsx
543
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
544
+
545
+ function DashboardLayout({ children }: { children?: React.ReactNode }) {
546
+ return (
547
+ <div className="dashboard">
548
+ <aside>
549
+ <ParallelOutlet name="@sidebar" />
550
+ </aside>
551
+ <main>{children ?? <Outlet />}</main>
552
+ <ParallelOutlet name="@notifications" />
553
+ </div>
554
+ );
555
+ }
556
+ ```
557
+
558
+ ### useOutlet()
559
+
560
+ Access outlet content programmatically:
561
+
562
+ ```tsx
563
+ "use client";
564
+ import { useOutlet } from "@rangojs/router/client";
565
+
566
+ function ConditionalLayout() {
567
+ const outlet = useOutlet();
568
+ // ReactNode | null
569
+
570
+ return outlet ? (
571
+ <div className="with-content">{outlet}</div>
572
+ ) : (
573
+ <div className="empty">No content</div>
574
+ );
575
+ }
576
+ ```
577
+
578
+ ## URL Hooks
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
+
646
+ ### useHref()
647
+
648
+ Mount-aware href for client components inside `include()` scopes:
649
+
650
+ ```tsx
651
+ "use client";
652
+ import { useHref, href, Link } from "@rangojs/router/client";
653
+
654
+ // Inside include("/shop", shopPatterns)
655
+ function ShopNav() {
656
+ const href = useHref();
657
+
658
+ return (
659
+ <>
660
+ {/* Local paths - auto-prefixed with /shop */}
661
+ <Link to={href("/cart")}>Cart</Link>
662
+ <Link to={href("/product/widget")}>Widget</Link>
663
+ </>
664
+ );
665
+ }
666
+ ```
667
+
668
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
669
+
670
+ ### useMount()
671
+
672
+ Returns the current `include()` mount path:
673
+
674
+ ```tsx
675
+ "use client";
676
+ import { useMount } from "@rangojs/router/client";
677
+
678
+ function MountInfo() {
679
+ const mount = useMount(); // "/shop" inside include("/shop", ...)
680
+ return <span>Mounted at: {mount}</span>;
681
+ }
682
+ ```
683
+
684
+ See `/links` for full URL generation guide including server-side `ctx.reverse`.
685
+
686
+ ## Hook Summary
687
+
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 } |