@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -0,0 +1,273 @@
1
+ # Data Hooks
2
+
3
+ ### useLoader()
4
+
5
+ Access loader data (strict - data guaranteed):
6
+
7
+ ```tsx
8
+ "use client";
9
+ import { useLoader } from "@rangojs/router/client";
10
+ import { ProductLoader } from "../loaders/product";
11
+
12
+ function ProductPrice() {
13
+ const { data, isLoading, error } = useLoader(ProductLoader);
14
+
15
+ // data: T (guaranteed - throws if not in context)
16
+ // isLoading: boolean
17
+ // error: Error | null
18
+
19
+ return <span>${data.price}</span>;
20
+ }
21
+ ```
22
+
23
+ **Precondition**: Loader must be registered on route via `loader()` helper.
24
+
25
+ Loaders can also be passed as props from server to client components:
26
+
27
+ ```tsx
28
+ "use client";
29
+ import { useLoader } from "@rangojs/router/client";
30
+ import type { ProductLoader } from "../loaders";
31
+
32
+ // typeof infers the full data type from the loader definition
33
+ function ProductCard({ loader }: { loader: typeof ProductLoader }) {
34
+ const { data } = useLoader(loader);
35
+ return <h2>{data.product.name}</h2>;
36
+ }
37
+ ```
38
+
39
+ ### useFetchLoader()
40
+
41
+ Access loader with on-demand fetching (flexible):
42
+
43
+ ```tsx
44
+ "use client";
45
+ import { useFetchLoader } from "@rangojs/router/client";
46
+ import { SearchLoader } from "../loaders/search";
47
+
48
+ function SearchResults() {
49
+ const { data, load, isLoading, error } = useFetchLoader(SearchLoader);
50
+
51
+ // data: T | undefined (may be undefined if not fetched)
52
+ // load: (options?) => Promise<T>
53
+ // refetch: alias for load
54
+
55
+ const handleSearch = async (query: string) => {
56
+ await load({ params: { query } });
57
+ };
58
+
59
+ return (
60
+ <div>
61
+ <input onChange={(e) => handleSearch(e.target.value)} />
62
+ {isLoading && <Spinner />}
63
+ {data?.results.map((r) => (
64
+ <Result key={r.id} {...r} />
65
+ ))}
66
+ </div>
67
+ );
68
+ }
69
+ ```
70
+
71
+ **Shared refetch behavior**:
72
+
73
+ When the loader is registered on the route via `loader()`, a plain
74
+ `load()` call (no options, or a trivially-defaulted GET with no
75
+ `params` and no `body`) broadcasts its result to every component
76
+ reading the same loader id. Layout, page, and parallel-slot reads
77
+ all converge on the new value:
78
+
79
+ ```tsx
80
+ // Layout button calls load() — the page read below sees the update too.
81
+ function Layout() {
82
+ const { data, load } = useLoader(CartLoader);
83
+ return <button onClick={() => load()}>Refresh ({data.count})</button>;
84
+ }
85
+ function Page() {
86
+ const { data } = useLoader(CartLoader); // updates with the layout's load()
87
+ return <span>{data.count} items</span>;
88
+ }
89
+ ```
90
+
91
+ `isLoading` and `error` follow the same scope. `throwOnError: true`
92
+ render-throws are scoped to the **originating** hook — sibling readers
93
+ see the error in their `error` state but their boundaries are not
94
+ triggered by someone else's failure. A successful follow-up `load()`
95
+ clears the shared error.
96
+
97
+ **`load()` calls that stay local** (no broadcast, per-hook state, same
98
+ semantics as the old per-component `useState`):
99
+
100
+ - `load({ params: { ... } })` — explicit params.
101
+ - `load({ method: "POST", body })` — mutations.
102
+ - Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
103
+ registered on the current route. Two unrelated components calling
104
+ `load()` on the same fetchable-but-unregistered loader keep
105
+ independent results.
106
+
107
+ So the search/list pattern still works — two components calling
108
+ `load({ params: { q } })` with different `q` values each keep their
109
+ own result; they do not collapse to last-write-wins through a shared
110
+ store.
111
+
112
+ **Scoping refetch with a `key`**:
113
+
114
+ Pass a `key` to partition the shared refresh store. Only hooks using the
115
+ **same** `key` refresh together when one of them calls `load()`. This is a
116
+ client-side refresh identity only — it never changes the request sent to the
117
+ server, and is unrelated to the server `cache({ key })` option and to
118
+ `revalidate()`.
119
+
120
+ ```tsx
121
+ // Two independent dashboards using the same loader. Without a key, one
122
+ // dashboard's load() would flip the other's spinner and value. With a key,
123
+ // they refresh independently.
124
+ function Dashboard({ id }: { id: string }) {
125
+ const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
126
+ return <button onClick={() => load()}>Refresh {data.total}</button>;
127
+ }
128
+ ```
129
+
130
+ The `key` widens sharing in two ways the default cannot:
131
+
132
+ - **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
133
+ with the same `q` in two components share one result and refresh together —
134
+ a keyed `load({ params: { q } })` broadcasts to the group instead of staying
135
+ local. (Mutations — non-GET or `body` — stay local even with a key.)
136
+ - **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
137
+ that is **not** registered on the route share too, letting unrelated
138
+ components opt into a common refresh group.
139
+
140
+ Lifecycle: a keyed read of an unregistered loader is reference-counted — its
141
+ shared value lives as long as at least one component using that key is mounted.
142
+ A persistent component (e.g. a header) keeps the value across navigations; a
143
+ route-scoped component's value is reclaimed when it unmounts. Registered-loader
144
+ reads (keyed or not) reset on navigation from fresh route data, as before.
145
+
146
+ **Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
147
+
148
+ `key` groups readers of one loader. To refresh **different** loaders together,
149
+ tag them with a shared `refreshGroup` name and trigger them with
150
+ `useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
151
+ function it returns, so one `useRefreshLoaders()` can refresh different groups
152
+ depending on context. A read may carry **several** tags — pass an array — and is
153
+ refreshed when **any** of its groups is refreshed:
154
+
155
+ ```tsx
156
+ function Profile() {
157
+ const { data } = useLoader(ProfileLoader, {
158
+ key: userId,
159
+ refreshGroup: "account",
160
+ });
161
+ return <span>{data.name}</span>;
162
+ }
163
+ function Orders() {
164
+ // Tagged into two groups: refreshed by "account" (the whole set) or the
165
+ // finer "orders" tag.
166
+ const { data } = useLoader(OrdersLoader, {
167
+ key: userId,
168
+ refreshGroup: ["account", "orders"],
169
+ });
170
+ return <span>{data.count} orders</span>;
171
+ }
172
+ function RefreshButtons() {
173
+ const refresh = useRefreshLoaders();
174
+ return (
175
+ <>
176
+ <button onClick={() => refresh("account")}>Refresh account</button>
177
+ <button onClick={() => refresh("orders")}>Refresh orders only</button>
178
+ <button onClick={() => refresh(["account", "orders"])}>
179
+ Refresh both
180
+ </button>
181
+ </>
182
+ );
183
+ }
184
+ ```
185
+
186
+ `refresh(groups)` accepts one name or an array and re-runs every currently-mounted
187
+ member tagged with **any** of them, with a **plain GET** against the current route
188
+ URL — no params, no body, no mutation methods, because a group spans loaders with
189
+ different shapes. A member that sits in two of the requested groups is fetched
190
+ once (members are unioned and deduped by read). It returns a promise that resolves
191
+ when all members settle and **rejects with an `AggregateError`** if any fail;
192
+ group refresh never render-throws, so handle failures at the await site
193
+ (`await refresh("account").catch(...)`). Each failing member also exposes its
194
+ error via its own read's `error`.
195
+
196
+ Multiple tags give you granular vs. whole-set refresh from one place: a coarse
197
+ tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
198
+ subset. Sharing within a group is opt-in via `key`: members that share a `key`
199
+ share one value (and one fetch); a grouped reader **without** a `key` gets its own
200
+ private bucket, so a group refresh updates only that read and never leaks into
201
+ unrelated unkeyed reads of the same loader. A bucket may belong to several groups
202
+ at once (one read tagged with multiple names, or different reads tagging the same
203
+ keyed bucket with different names). Keep parameterized loaders on the single-loader
204
+ `key` — a plain-GET group refresh sends no params.
205
+
206
+ **Load options**:
207
+
208
+ ```tsx
209
+ // JSON body — sent as application/json, available as ctx.body on the server
210
+ await load({
211
+ method: "POST",
212
+ params: { query: "test" },
213
+ body: { data: "value" },
214
+ });
215
+
216
+ // FormData body — sent as multipart/form-data, available as ctx.formData on the server.
217
+ // Automatically detected: when body is a FormData instance, the request switches
218
+ // to multipart/form-data to preserve File objects and binary data.
219
+ const formData = new FormData();
220
+ formData.append("file", fileInput.files[0]);
221
+ await load({ method: "POST", body: formData });
222
+ ```
223
+
224
+ **Body type auto-switching**: The `load()` function inspects the `body` value to
225
+ choose the encoding. If `body instanceof FormData`, the request is sent as
226
+ `multipart/form-data` (browser sets the boundary header automatically). Otherwise
227
+ the body is JSON-serialized and sent with `Content-Type: application/json`. On the
228
+ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.formData`.
229
+
230
+ **File upload example**:
231
+
232
+ ```tsx
233
+ "use client";
234
+ import { useFetchLoader } from "@rangojs/router/client";
235
+ import { FileUploadLoader } from "../loaders/upload";
236
+
237
+ function FileUploader() {
238
+ const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
239
+ const formRef = useRef<HTMLFormElement>(null);
240
+
241
+ const handleSubmit = async (formData: FormData) => {
242
+ await load({ method: "POST", body: formData });
243
+ formRef.current?.reset();
244
+ };
245
+
246
+ return (
247
+ <form ref={formRef} action={handleSubmit}>
248
+ <input type="file" name="file" />
249
+ <button type="submit" disabled={isLoading}>
250
+ {isLoading ? "Uploading..." : "Upload"}
251
+ </button>
252
+ {data?.uploadedFile && <p>Uploaded: {data.uploadedFile.name}</p>}
253
+ </form>
254
+ );
255
+ }
256
+ ```
257
+
258
+ Server-side loader for the upload:
259
+
260
+ ```typescript
261
+ import { createLoader } from "@rangojs/router";
262
+
263
+ export const FileUploadLoader = createLoader(async (ctx) => {
264
+ "use server";
265
+
266
+ const file = ctx.formData?.get("file") as File | null;
267
+ if (file && file.size > 0) {
268
+ // Process file (save to R2, D1, etc.)
269
+ return { uploadedFile: { name: file.name, size: file.size } };
270
+ }
271
+ return { uploadedFile: null };
272
+ }, true); // true = fetchable (can be called from the client via load())
273
+ ```
@@ -0,0 +1,103 @@
1
+ # Handle and Action Hooks
2
+
3
+ ## Handle Hooks
4
+
5
+ ### useHandle()
6
+
7
+ Access accumulated handle data from route segments:
8
+
9
+ ```tsx
10
+ "use client";
11
+ import { useHandle, Breadcrumbs } from "@rangojs/router/client";
12
+
13
+ function BreadcrumbNav() {
14
+ const crumbs = useHandle(Breadcrumbs);
15
+ // Array of { label, href } accumulated from layouts/routes
16
+
17
+ return (
18
+ <nav>
19
+ {crumbs.map((c, i) => (
20
+ <span key={i}>
21
+ <a href={c.href}>{c.label}</a>
22
+ {i < crumbs.length - 1 && " > "}
23
+ </span>
24
+ ))}
25
+ </nav>
26
+ );
27
+ }
28
+
29
+ // With selector
30
+ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
31
+ ```
32
+
33
+ Handles can be passed as props from server to client components:
34
+
35
+ ```tsx
36
+ // Server component
37
+ path("/dashboard", (ctx) => {
38
+ const push = ctx.use(Breadcrumbs);
39
+ push({ label: "Dashboard", href: "/dashboard" });
40
+ return <DashboardNav handle={Breadcrumbs} />;
41
+ });
42
+ ```
43
+
44
+ ```tsx
45
+ // Client component — typeof infers the full Handle<T> type
46
+ "use client";
47
+ import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
48
+
49
+ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
50
+ const crumbs = useHandle(handle);
51
+ return (
52
+ <nav>
53
+ {crumbs.map((c) => (
54
+ <a href={c.href}>{c.label}</a>
55
+ ))}
56
+ </nav>
57
+ );
58
+ }
59
+ ```
60
+
61
+ RSC serialization strips the `collect` function via `toJSON()`. On the client,
62
+ `useHandle()` recovers it from the module-level registry (populated when
63
+ `createHandle()` runs during module initialization).
64
+
65
+ ## Action Hooks
66
+
67
+ For the full server-action guide (defining actions, `useActionState`,
68
+ `useOptimistic`, validation, revalidation, error handling, file uploads),
69
+ see `/server-actions`. `useAction()` below is a Rango-specific hook for
70
+ tracking actions called outside a `<form action={...}>` flow.
71
+
72
+ ### useAction()
73
+
74
+ Track state of server action invocations:
75
+
76
+ ```tsx
77
+ "use client";
78
+ import { useAction } from "@rangojs/router/client";
79
+ import { addToCart } from "../actions/cart";
80
+
81
+ function AddToCartButton({ productId }: { productId: string }) {
82
+ const { state, error, result } = useAction(addToCart);
83
+
84
+ // state: 'idle' | 'loading' | 'streaming'
85
+ // actionId: string | null
86
+ // payload: unknown | null (input data)
87
+ // error: Error | null
88
+ // result: unknown | null (return value)
89
+
90
+ return (
91
+ <form action={addToCart}>
92
+ <input type="hidden" name="productId" value={productId} />
93
+ <button disabled={state === "loading"}>
94
+ {state === "loading" ? "Adding..." : "Add to Cart"}
95
+ </button>
96
+ {error && <p className="error">{error.message}</p>}
97
+ </form>
98
+ );
99
+ }
100
+
101
+ // Match by string suffix (convenient but may be ambiguous)
102
+ const isLoading = useAction("addToCart", (s) => s.state === "loading");
103
+ ```
@@ -0,0 +1,110 @@
1
+ # Navigation Hooks
2
+
3
+ ### useNavigation()
4
+
5
+ Track reactive navigation state (state-only, no actions):
6
+
7
+ ```tsx
8
+ "use client";
9
+ import { useNavigation } from "@rangojs/router/client";
10
+
11
+ function NavIndicator() {
12
+ const nav = useNavigation();
13
+
14
+ // State properties
15
+ nav.state; // 'idle' | 'loading'
16
+ nav.isStreaming; // boolean
17
+ nav.location; // Current URL
18
+ nav.pendingUrl; // Target URL during navigation (or null)
19
+
20
+ return nav.state === "loading" ? <Spinner /> : null;
21
+ }
22
+
23
+ // With selector for performance (re-renders only when selected value changes)
24
+ function IsLoading() {
25
+ const isLoading = useNavigation((nav) => nav.state === "loading");
26
+ return isLoading ? <Spinner /> : null;
27
+ }
28
+ ```
29
+
30
+ ### useRouter()
31
+
32
+ Access stable router actions (never causes re-renders):
33
+
34
+ ```tsx
35
+ "use client";
36
+ import { useRouter } from "@rangojs/router/client";
37
+
38
+ function NavigationControls() {
39
+ const router = useRouter();
40
+
41
+ router.push("/products"); // Navigate (adds history entry)
42
+ router.replace("/login"); // Navigate (replaces history entry)
43
+ router.refresh(); // Re-fetch current route data
44
+ router.prefetch("/dashboard"); // Prefetch for faster navigation
45
+ router.back(); // Go back in history
46
+ router.forward(); // Go forward in history
47
+ }
48
+ ```
49
+
50
+ #### Skipping revalidation
51
+
52
+ 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.
53
+
54
+ ```tsx
55
+ // Update search params without server round-trip
56
+ router.push("/products?color=blue", { revalidate: false });
57
+ router.replace("/products?page=3", { revalidate: false });
58
+ ```
59
+
60
+ If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
61
+
62
+ ```tsx
63
+ <Link to="/products?color=blue" revalidate={false}>
64
+ Blue
65
+ </Link>
66
+ ```
67
+
68
+ Plain `<a>` tags can opt in via `data-revalidate="false"`.
69
+
70
+ ### useSegments()
71
+
72
+ Access current URL path and matched route segments:
73
+
74
+ ```tsx
75
+ "use client";
76
+ import { useSegments } from "@rangojs/router/client";
77
+
78
+ function Breadcrumbs() {
79
+ const { path, segmentIds, location } = useSegments();
80
+
81
+ // path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
82
+ // segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
83
+ // location: URL object
84
+
85
+ return <nav>{path.join(" > ")}</nav>;
86
+ }
87
+
88
+ // With selector
89
+ const isShopRoute = useSegments((s) => s.path[0] === "shop");
90
+ ```
91
+
92
+ ### useLinkStatus()
93
+
94
+ Track pending state inside a Link component:
95
+
96
+ ```tsx
97
+ "use client";
98
+ import { Link, useLinkStatus } from "@rangojs/router/client";
99
+
100
+ function LoadingIndicator() {
101
+ const { pending } = useLinkStatus();
102
+ return pending ? <Spinner /> : null;
103
+ }
104
+
105
+ // Must be inside Link
106
+ <Link to="/dashboard">
107
+ Dashboard
108
+ <LoadingIndicator />
109
+ </Link>;
110
+ ```
@@ -0,0 +1,41 @@
1
+ # Outlet Components
2
+
3
+ ### Outlet / ParallelOutlet
4
+
5
+ Render child content in layouts:
6
+
7
+ ```tsx
8
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
9
+
10
+ function DashboardLayout({ children }: { children?: React.ReactNode }) {
11
+ return (
12
+ <div className="dashboard">
13
+ <aside>
14
+ <ParallelOutlet name="@sidebar" />
15
+ </aside>
16
+ <main>{children ?? <Outlet />}</main>
17
+ <ParallelOutlet name="@notifications" />
18
+ </div>
19
+ );
20
+ }
21
+ ```
22
+
23
+ ### useOutlet()
24
+
25
+ Access outlet content programmatically:
26
+
27
+ ```tsx
28
+ "use client";
29
+ import { useOutlet } from "@rangojs/router/client";
30
+
31
+ function ConditionalLayout() {
32
+ const outlet = useOutlet();
33
+ // ReactNode | null
34
+
35
+ return outlet ? (
36
+ <div className="with-content">{outlet}</div>
37
+ ) : (
38
+ <div className="empty">No content</div>
39
+ );
40
+ }
41
+ ```