@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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 (402) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3373 -1176
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +68 -14
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +224 -32
  13. package/skills/caching/SKILL.md +279 -17
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +235 -9
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +771 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +77 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +70 -5
  37. package/skills/router-setup/SKILL.md +65 -8
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +322 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +186 -100
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +165 -112
  80. package/src/browser/prefetch/cache.ts +205 -62
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +42 -8
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +102 -15
  85. package/src/browser/react/NavigationProvider.tsx +295 -119
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  89. package/src/browser/react/filter-segment-order.ts +66 -7
  90. package/src/browser/react/index.ts +0 -48
  91. package/src/browser/react/location-state-shared.ts +178 -8
  92. package/src/browser/react/location-state.ts +39 -14
  93. package/src/browser/react/use-action.ts +6 -15
  94. package/src/browser/react/use-handle.ts +23 -69
  95. package/src/browser/react/use-href.tsx +8 -1
  96. package/src/browser/react/use-link-status.ts +33 -8
  97. package/src/browser/react/use-navigation.ts +32 -7
  98. package/src/browser/react/use-params.ts +20 -10
  99. package/src/browser/react/use-reverse.ts +106 -0
  100. package/src/browser/react/use-router.ts +46 -11
  101. package/src/browser/react/use-search-params.ts +0 -5
  102. package/src/browser/react/use-segments.ts +11 -21
  103. package/src/browser/response-adapter.ts +99 -8
  104. package/src/browser/rsc-router.tsx +114 -24
  105. package/src/browser/scroll-restoration.ts +37 -22
  106. package/src/browser/segment-reconciler.ts +36 -14
  107. package/src/browser/segment-structure-assert.ts +2 -2
  108. package/src/browser/server-action-bridge.ts +222 -72
  109. package/src/browser/types.ts +102 -12
  110. package/src/browser/validate-redirect-origin.ts +43 -16
  111. package/src/build/collect-fallback-refs.ts +107 -0
  112. package/src/build/generate-manifest.ts +65 -40
  113. package/src/build/generate-route-types.ts +5 -1
  114. package/src/build/index.ts +8 -2
  115. package/src/build/prefix-tree-utils.ts +123 -0
  116. package/src/build/route-trie.ts +165 -36
  117. package/src/build/route-types/ast-route-extraction.ts +15 -8
  118. package/src/build/route-types/codegen.ts +16 -5
  119. package/src/build/route-types/include-resolution.ts +125 -24
  120. package/src/build/route-types/param-extraction.ts +6 -3
  121. package/src/build/route-types/per-module-writer.ts +22 -6
  122. package/src/build/route-types/router-processing.ts +260 -94
  123. package/src/build/route-types/scan-filter.ts +9 -2
  124. package/src/build/route-types/source-scan.ts +216 -0
  125. package/src/build/runtime-discovery.ts +9 -20
  126. package/src/cache/cache-error.ts +104 -0
  127. package/src/cache/cache-key-utils.ts +29 -13
  128. package/src/cache/cache-policy.ts +108 -34
  129. package/src/cache/cache-runtime.ts +224 -41
  130. package/src/cache/cache-scope.ts +188 -82
  131. package/src/cache/cache-tag.ts +103 -0
  132. package/src/cache/cf/cf-base64.ts +33 -0
  133. package/src/cache/cf/cf-cache-constants.ts +127 -0
  134. package/src/cache/cf/cf-cache-store.ts +1989 -378
  135. package/src/cache/cf/cf-cache-types.ts +349 -0
  136. package/src/cache/cf/cf-kv-utils.ts +46 -0
  137. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  138. package/src/cache/cf/index.ts +6 -16
  139. package/src/cache/document-cache.ts +89 -21
  140. package/src/cache/handle-snapshot.ts +70 -0
  141. package/src/cache/index.ts +10 -20
  142. package/src/cache/memory-segment-store.ts +136 -37
  143. package/src/cache/profile-registry.ts +46 -31
  144. package/src/cache/read-through-swr.ts +56 -12
  145. package/src/cache/segment-codec.ts +9 -17
  146. package/src/cache/tag-invalidation.ts +230 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +18 -6
  155. package/src/decode-loader-results.ts +52 -0
  156. package/src/defer.ts +196 -0
  157. package/src/deps/ssr.ts +0 -1
  158. package/src/encode-kv.ts +49 -0
  159. package/src/errors.ts +30 -4
  160. package/src/escape-script.ts +52 -0
  161. package/src/handle.ts +70 -22
  162. package/src/handles/MetaTags.tsx +62 -19
  163. package/src/handles/Scripts.tsx +183 -0
  164. package/src/handles/breadcrumbs.ts +37 -8
  165. package/src/handles/is-thenable.ts +19 -0
  166. package/src/handles/meta.ts +51 -40
  167. package/src/handles/script.ts +244 -0
  168. package/src/host/cookie-handler.ts +9 -60
  169. package/src/host/errors.ts +0 -24
  170. package/src/host/index.ts +8 -2
  171. package/src/host/pattern-matcher.ts +23 -52
  172. package/src/host/router.ts +107 -99
  173. package/src/host/testing.ts +40 -27
  174. package/src/host/types.ts +37 -4
  175. package/src/host/utils.ts +1 -1
  176. package/src/href-client.ts +137 -22
  177. package/src/index.rsc.ts +99 -13
  178. package/src/index.ts +139 -19
  179. package/src/internal-debug.ts +11 -10
  180. package/src/loader-store.ts +500 -0
  181. package/src/loader.rsc.ts +20 -13
  182. package/src/loader.ts +12 -11
  183. package/src/missing-id-error.ts +68 -0
  184. package/src/outlet-context.ts +1 -1
  185. package/src/outlet-provider.tsx +1 -5
  186. package/src/prerender/param-hash.ts +16 -16
  187. package/src/prerender/store.ts +37 -41
  188. package/src/prerender.ts +198 -82
  189. package/src/redirect-origin.ts +100 -0
  190. package/src/regex-escape.ts +8 -0
  191. package/src/render-error-thrower.tsx +20 -0
  192. package/src/response-utils.ts +62 -0
  193. package/src/reverse.ts +65 -15
  194. package/src/root-error-boundary.tsx +1 -19
  195. package/src/route-content-wrapper.tsx +19 -77
  196. package/src/route-definition/dsl-helpers.ts +461 -304
  197. package/src/route-definition/helper-factories.ts +28 -140
  198. package/src/route-definition/helpers-types.ts +143 -69
  199. package/src/route-definition/index.ts +4 -2
  200. package/src/route-definition/redirect.ts +51 -10
  201. package/src/route-definition/resolve-handler-use.ts +160 -0
  202. package/src/route-definition/use-item-types.ts +29 -0
  203. package/src/route-map-builder.ts +0 -16
  204. package/src/route-types.ts +37 -46
  205. package/src/router/basename.ts +14 -0
  206. package/src/router/content-negotiation.ts +164 -17
  207. package/src/router/error-handling.ts +45 -18
  208. package/src/router/find-match.ts +44 -23
  209. package/src/router/handler-context.ts +52 -31
  210. package/src/router/instrument.ts +350 -0
  211. package/src/router/intercept-resolution.ts +48 -24
  212. package/src/router/lazy-includes.ts +15 -52
  213. package/src/router/loader-resolution.ts +268 -56
  214. package/src/router/logging.ts +0 -6
  215. package/src/router/manifest.ts +40 -42
  216. package/src/router/match-api.ts +124 -204
  217. package/src/router/match-context.ts +0 -22
  218. package/src/router/match-handlers.ts +58 -58
  219. package/src/router/match-middleware/background-revalidation.ts +40 -24
  220. package/src/router/match-middleware/cache-lookup.ts +170 -276
  221. package/src/router/match-middleware/cache-store.ts +64 -52
  222. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  223. package/src/router/match-middleware/segment-resolution.ts +45 -14
  224. package/src/router/match-pipelines.ts +1 -42
  225. package/src/router/match-result.ts +87 -39
  226. package/src/router/metrics.ts +0 -34
  227. package/src/router/middleware-types.ts +7 -140
  228. package/src/router/middleware.ts +266 -169
  229. package/src/router/navigation-snapshot.ts +131 -0
  230. package/src/router/params-util.ts +23 -0
  231. package/src/router/pattern-matching.ts +132 -90
  232. package/src/router/prefetch-cache-ttl.ts +51 -0
  233. package/src/router/prerender-match.ts +195 -56
  234. package/src/router/preview-match.ts +32 -102
  235. package/src/router/request-classification.ts +276 -0
  236. package/src/router/revalidation.ts +123 -73
  237. package/src/router/route-snapshot.ts +244 -0
  238. package/src/router/router-context.ts +3 -28
  239. package/src/router/router-interfaces.ts +115 -35
  240. package/src/router/router-options.ts +172 -15
  241. package/src/router/router-registry.ts +2 -5
  242. package/src/router/segment-resolution/fresh.ts +162 -84
  243. package/src/router/segment-resolution/helpers.ts +86 -6
  244. package/src/router/segment-resolution/loader-cache.ts +76 -39
  245. package/src/router/segment-resolution/revalidation.ts +351 -321
  246. package/src/router/segment-resolution/static-store.ts +19 -5
  247. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  248. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  249. package/src/router/segment-resolution.ts +5 -1
  250. package/src/router/segment-wrappers.ts +6 -5
  251. package/src/router/state-cookie-name.ts +33 -0
  252. package/src/router/substitute-pattern-params.ts +56 -0
  253. package/src/router/telemetry-otel.ts +161 -199
  254. package/src/router/telemetry.ts +96 -19
  255. package/src/router/timeout.ts +0 -20
  256. package/src/router/tracing.ts +206 -0
  257. package/src/router/trie-matching.ts +163 -59
  258. package/src/router/types.ts +9 -63
  259. package/src/router/url-params.ts +44 -0
  260. package/src/router.ts +157 -54
  261. package/src/rsc/handler-context.ts +3 -2
  262. package/src/rsc/handler.ts +655 -529
  263. package/src/rsc/helpers.ts +168 -46
  264. package/src/rsc/index.ts +2 -5
  265. package/src/rsc/json-route-result.ts +38 -0
  266. package/src/rsc/loader-fetch.ts +122 -31
  267. package/src/rsc/manifest-init.ts +33 -42
  268. package/src/rsc/origin-guard.ts +39 -25
  269. package/src/rsc/progressive-enhancement.ts +131 -14
  270. package/src/rsc/redirect-guard.ts +99 -0
  271. package/src/rsc/response-cache-serve.ts +238 -0
  272. package/src/rsc/response-error.ts +79 -12
  273. package/src/rsc/response-route-handler.ts +99 -189
  274. package/src/rsc/rsc-rendering.ts +109 -74
  275. package/src/rsc/runtime-warnings.ts +23 -10
  276. package/src/rsc/server-action.ts +287 -115
  277. package/src/rsc/ssr-setup.ts +18 -2
  278. package/src/rsc/transition-gate.ts +89 -0
  279. package/src/rsc/types.ts +29 -9
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +236 -202
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +224 -52
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +38 -46
  291. package/src/server/request-context.ts +401 -173
  292. package/src/ssr/index.tsx +24 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +105 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +357 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/run-transition-when.ts +164 -0
  320. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  321. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  322. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  323. package/src/testing/vitest-stubs/version.ts +5 -0
  324. package/src/testing/vitest.ts +305 -0
  325. package/src/theme/ThemeProvider.tsx +20 -58
  326. package/src/theme/ThemeScript.tsx +7 -9
  327. package/src/theme/constants.ts +52 -13
  328. package/src/theme/index.ts +0 -7
  329. package/src/theme/theme-context.ts +1 -5
  330. package/src/theme/theme-script.ts +22 -21
  331. package/src/theme/use-theme.ts +0 -3
  332. package/src/types/boundaries.ts +0 -35
  333. package/src/types/cache-types.ts +17 -8
  334. package/src/types/error-types.ts +30 -90
  335. package/src/types/global-namespace.ts +54 -41
  336. package/src/types/handler-context.ts +125 -71
  337. package/src/types/index.ts +3 -10
  338. package/src/types/loader-types.ts +40 -11
  339. package/src/types/request-scope.ts +112 -0
  340. package/src/types/route-config.ts +6 -50
  341. package/src/types/route-entry.ts +12 -7
  342. package/src/types/segments.ts +136 -15
  343. package/src/urls/include-helper.ts +33 -70
  344. package/src/urls/index.ts +1 -11
  345. package/src/urls/path-helper-types.ts +68 -18
  346. package/src/urls/path-helper.ts +57 -111
  347. package/src/urls/pattern-types.ts +48 -19
  348. package/src/urls/response-types.ts +25 -22
  349. package/src/urls/type-extraction.ts +58 -139
  350. package/src/urls/urls-function.ts +1 -19
  351. package/src/use-loader.tsx +346 -89
  352. package/src/vite/debug.ts +185 -0
  353. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  354. package/src/vite/discovery/discover-routers.ts +130 -85
  355. package/src/vite/discovery/discovery-errors.ts +194 -0
  356. package/src/vite/discovery/gate-state.ts +171 -0
  357. package/src/vite/discovery/prerender-collection.ts +214 -132
  358. package/src/vite/discovery/route-types-writer.ts +40 -84
  359. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  360. package/src/vite/discovery/state.ts +57 -4
  361. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  362. package/src/vite/index.ts +6 -0
  363. package/src/vite/inject-client-debug.ts +36 -0
  364. package/src/vite/plugin-types.ts +178 -5
  365. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  366. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  367. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  369. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  370. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  371. package/src/vite/plugins/expose-action-id.ts +48 -95
  372. package/src/vite/plugins/expose-id-utils.ts +96 -51
  373. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  374. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  375. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  376. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  377. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  378. package/src/vite/plugins/performance-tracks.ts +64 -170
  379. package/src/vite/plugins/refresh-cmd.ts +89 -27
  380. package/src/vite/plugins/use-cache-transform.ts +73 -83
  381. package/src/vite/plugins/version-injector.ts +40 -29
  382. package/src/vite/plugins/version-plugin.ts +37 -40
  383. package/src/vite/plugins/virtual-entries.ts +39 -25
  384. package/src/vite/rango.ts +118 -114
  385. package/src/vite/router-discovery.ts +941 -142
  386. package/src/vite/utils/ast-handler-extract.ts +26 -35
  387. package/src/vite/utils/banner.ts +1 -1
  388. package/src/vite/utils/bundle-analysis.ts +10 -15
  389. package/src/vite/utils/client-chunks.ts +184 -0
  390. package/src/vite/utils/directive-prologue.ts +40 -0
  391. package/src/vite/utils/forward-user-plugins.ts +171 -0
  392. package/src/vite/utils/manifest-utils.ts +4 -59
  393. package/src/vite/utils/package-resolution.ts +20 -52
  394. package/src/vite/utils/prerender-utils.ts +81 -34
  395. package/src/vite/utils/shared-utils.ts +92 -42
  396. package/src/browser/action-response-classifier.ts +0 -99
  397. package/src/browser/debug-channel.ts +0 -93
  398. package/src/browser/react/use-client-cache.ts +0 -58
  399. package/src/browser/shallow.ts +0 -40
  400. package/src/handles/index.ts +0 -7
  401. package/src/network-error-thrower.tsx +0 -23
  402. package/src/router/middleware-cookies.ts +0 -55
@@ -1,11 +1,70 @@
1
1
  /**
2
- * Filter segment IDs to only include routes and layouts.
3
- * Excludes parallels (contain .@) and loaders (contain D followed by digit).
2
+ * Build the handle-collection segment order from a raw `matched` list.
3
+ *
4
+ * Two responsibilities:
5
+ *
6
+ * 1. Drop loader sub-ids ("D" followed by a digit, e.g. "M0L0D1.user") —
7
+ * loaders never push handles.
8
+ *
9
+ * 2. Place each parallel slot id (contains ".@") immediately after its
10
+ * parent layout/route id. Raw segment-resolution emission order does NOT
11
+ * guarantee this: route-mounted parallels are resolved/pushed BEFORE the
12
+ * route handler's segment is appended (see fresh.ts:resolveSegment for
13
+ * routes, and revalidation.ts ~915-919), so matched can read
14
+ * `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
15
+ * with later-wins semantics, so without normalization the route handler's
16
+ * Meta would override the slot's more-specific Meta — backwards.
17
+ *
18
+ * Slot-id format is `<parentShortCode>.@<slotName>`; `parentShortCode` never
19
+ * contains ".@", so splitting at the first ".@" reliably yields the parent.
4
20
  */
5
21
  export function filterSegmentOrder(matched: string[]): string[] {
6
- return matched.filter((id) => {
7
- if (id.includes(".@")) return false;
8
- if (/D\d+\./.test(id)) return false;
9
- return true;
10
- });
22
+ const slotsByParent = new Map<string, string[]>();
23
+ const nonSlots: string[] = [];
24
+ const nonSlotSet = new Set<string>();
25
+
26
+ for (const id of matched) {
27
+ if (/D\d+\./.test(id)) continue;
28
+ const slotIdx = id.indexOf(".@");
29
+ if (slotIdx >= 0) {
30
+ const parent = id.slice(0, slotIdx);
31
+ const list = slotsByParent.get(parent);
32
+ if (list) {
33
+ list.push(id);
34
+ } else {
35
+ slotsByParent.set(parent, [id]);
36
+ }
37
+ } else {
38
+ nonSlots.push(id);
39
+ nonSlotSet.add(id);
40
+ }
41
+ }
42
+
43
+ const result: string[] = [];
44
+ for (const id of nonSlots) {
45
+ result.push(id);
46
+ const slots = slotsByParent.get(id);
47
+ if (slots) result.push(...slots);
48
+ }
49
+ for (const [parent, slots] of slotsByParent) {
50
+ if (!nonSlotSet.has(parent)) result.push(...slots);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Build the "layouts and routes only" id list for useSegments().segmentIds.
57
+ *
58
+ * Strips parallel slot ids (contain ".@") and loader sub-ids ("D" followed by
59
+ * a digit, e.g. "M0L0D1.user") without reordering. Distinct from
60
+ * filterSegmentOrder, which also reorders slots after their parent for handle
61
+ * collection.
62
+ *
63
+ * Shared by SSR (ssr/index.tsx) and the client event controller
64
+ * (event-controller.ts) so both produce identical output; if they diverge,
65
+ * useSegments().segmentIds rendered during SSR and after hydration disagree
66
+ * and React reports a hydration mismatch.
67
+ */
68
+ export function filterRouteSegmentIds(matched: string[]): string[] {
69
+ return matched.filter((id) => !id.includes(".@") && !/D\d+\./.test(id));
11
70
  }
@@ -1,52 +1,4 @@
1
- // React exports for browser navigation
2
-
3
- // Hook with Zustand-style selectors
4
- export { useNavigation } from "./use-navigation.js";
5
-
6
- // Router actions hook (stable reference, no re-renders)
7
- export { useRouter } from "./use-router.js";
8
-
9
- // URL hooks
10
- export { usePathname } from "./use-pathname.js";
11
- export { useSearchParams } from "./use-search-params.js";
12
- export { useParams } from "./use-params.js";
13
-
14
- // Action state tracking hook
15
- export { useAction, type TrackedActionState } from "./use-action.js";
16
-
17
- // Segments state hook
18
- export { useSegments, type SegmentsState } from "./use-segments.js";
19
-
20
- // Handle data hook
21
- export { useHandle } from "./use-handle.js";
22
-
23
- // Client cache controls hook
24
- export {
25
- useClientCache,
26
- type ClientCacheControls,
27
- } from "./use-client-cache.js";
28
-
29
- // Provider
30
1
  export {
31
2
  NavigationProvider,
32
3
  type NavigationProviderProps,
33
4
  } from "./NavigationProvider.js";
34
-
35
- // Context (for advanced usage)
36
- export {
37
- NavigationStoreContext,
38
- type NavigationStoreContextValue,
39
- } from "./context.js";
40
-
41
- // Link component
42
- export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
43
-
44
- // Link status hook
45
- export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
46
-
47
- // Scroll restoration
48
- export {
49
- ScrollRestoration,
50
- useScrollRestoration,
51
- type ScrollRestorationProps,
52
- } from "./ScrollRestoration.js";
@@ -1,7 +1,4 @@
1
- /**
2
- * Shared location state utilities - works in both RSC and client contexts
3
- * No "use client" directive so it can be imported from RSC
4
- */
1
+ import type { ReactElement } from "react";
5
2
 
6
3
  /**
7
4
  * Internal entry representing a state value with its unique key.
@@ -22,6 +19,80 @@ export interface LocationStateOptions {
22
19
  flash?: boolean;
23
20
  }
24
21
 
22
+ type LocationStateUnsafeFn = (...args: never[]) => unknown;
23
+
24
+ type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
25
+
26
+ type IsAny<T> = 0 extends 1 & T ? true : false;
27
+ type IsUnknown<T> =
28
+ IsAny<T> extends true ? false : unknown extends T ? true : false;
29
+
30
+ /**
31
+ * Branded error surfaced when a value that cannot live in location state is
32
+ * used. Location state is written into `history.state`, which uses the
33
+ * structured clone algorithm; React elements, functions, and symbols throw a
34
+ * `DataCloneError` at runtime. Carries a human-readable reason so the compile
35
+ * error explains the fix.
36
+ */
37
+ export type LocationStateUnsafe<Reason extends string> = {
38
+ readonly __rango_location_state_unsafe: Reason;
39
+ };
40
+
41
+ /**
42
+ * Maps `T` to itself when it is safe to store in location state, or to a branded
43
+ * {@link LocationStateUnsafe} error for the disallowed parts: `unknown`, React
44
+ * elements (RSC/JSX content), functions, class constructors, and symbols.
45
+ * Recurses through arrays, `Map`, `Set`, and plain objects; structured-clone
46
+ * built-ins (`Date`, `RegExp`, typed arrays, `Blob`, `File`, `FormData`) pass
47
+ * through. Consumed by {@link ValidateLocationState}, which is intersected into a
48
+ * definition's value parameter so posting RSC content is a COMPILE error, not a
49
+ * runtime `DataCloneError`. (`any` is unguardable and remains an escape hatch.)
50
+ */
51
+ export type LocationStateSafe<T> =
52
+ IsUnknown<T> extends true
53
+ ? LocationStateUnsafe<"location state needs an explicit, concrete type; `unknown` cannot be verified as serializable">
54
+ : T extends LocationStateUnsafeFn
55
+ ? LocationStateUnsafe<"functions cannot be stored in location state">
56
+ : T extends LocationStateUnsafeCtor
57
+ ? LocationStateUnsafe<"class constructors cannot be stored in location state">
58
+ : T extends symbol
59
+ ? LocationStateUnsafe<"symbols cannot be stored in location state">
60
+ : T extends ReactElement
61
+ ? LocationStateUnsafe<"React/RSC content cannot be stored in location state; store plain data and render it on arrival">
62
+ : T extends string | number | boolean | bigint | null | undefined
63
+ ? T
64
+ : T extends
65
+ | Date
66
+ | RegExp
67
+ | ArrayBuffer
68
+ | ArrayBufferView
69
+ | Blob
70
+ | File
71
+ | FormData
72
+ ? T
73
+ : T extends ReadonlyMap<infer K, infer V>
74
+ ? ReadonlyMap<LocationStateSafe<K>, LocationStateSafe<V>>
75
+ : T extends ReadonlySet<infer V>
76
+ ? ReadonlySet<LocationStateSafe<V>>
77
+ : T extends readonly unknown[]
78
+ ? { [K in keyof T]: LocationStateSafe<T[K]> }
79
+ : T extends object
80
+ ? { [K in keyof T]: LocationStateSafe<T[K]> }
81
+ : T;
82
+
83
+ /**
84
+ * `unknown` (a no-op) when `T` is safe to store in location state, otherwise a
85
+ * branded {@link LocationStateUnsafe} object. Intersected into the value
86
+ * parameter of a definition's call and `write()` so POSTING RSC content (or any
87
+ * non-serializable value) is a compile error whose text carries the reason —
88
+ * without a `TState extends ...` self-constraint, which TypeScript rejects as
89
+ * circular (TS2313). For safe `T`, `value & unknown` collapses back to `value`,
90
+ * so valid usage is unchanged.
91
+ */
92
+ export type ValidateLocationState<T> = [T] extends [LocationStateSafe<T>]
93
+ ? unknown
94
+ : LocationStateUnsafe<"location state must be serializable: React/RSC content, functions, and symbols cannot be stored — pass plain data and render it on arrival">;
95
+
25
96
  /**
26
97
  * Type-safe location state definition
27
98
  *
@@ -34,8 +105,43 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
34
105
  __rsc_ls_key: string;
35
106
  /** Whether this state auto-clears after first read */
36
107
  readonly __rsc_ls_flash: boolean;
37
- /** Read the current value from history.state (client-side only, undefined during SSR) */
108
+ /**
109
+ * Read the current value from history.state.
110
+ *
111
+ * Returns undefined during SSR (no `window`). To stay hydration-safe, do
112
+ * NOT call read() inline during the initial render — the server returns
113
+ * undefined while the client may have a value preserved in history.state
114
+ * (e.g. after a hard reload of an entry that earlier called write()),
115
+ * which causes a hydration mismatch. Call read() inside an event handler
116
+ * or a useEffect post-mount instead, or use useLocationState() if you
117
+ * want React to manage subscription/hydration for you.
118
+ */
38
119
  read(): TState | undefined;
120
+ /**
121
+ * Statically write the value into the current history entry under this
122
+ * definition's key, preserving any other keys already on history.state
123
+ * (e.g. router bookkeeping, other LocationState slots).
124
+ *
125
+ * This is the non-reactive counterpart to read(): it does not dispatch any
126
+ * event, so components reading via useLocationState() will NOT re-render
127
+ * until the next navigation/popstate. Use it when you only need the value
128
+ * to be there on the next read() or on the next mount (including after
129
+ * back/forward and hard refresh of the same entry).
130
+ *
131
+ * Client-only: throws when called on the server (no history available).
132
+ */
133
+ write(value: TState & ValidateLocationState<TState>): void;
134
+ /**
135
+ * Statically remove this definition's slot from the current history entry,
136
+ * leaving any other keys on history.state untouched. Idempotent: removing
137
+ * a slot that isn't present is a no-op.
138
+ *
139
+ * Same non-reactive semantics as write(): no event is dispatched, so
140
+ * useLocationState() readers will NOT re-render until the next navigation.
141
+ *
142
+ * Client-only: throws when called on the server (no history available).
143
+ */
144
+ delete(): void;
39
145
  }
40
146
 
41
147
  /**
@@ -70,18 +176,30 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
70
176
  *
71
177
  * // Read without hook (snapshot, client-side only)
72
178
  * const snap = ProductState.read();
179
+ *
180
+ * // Static write to current history entry (non-reactive, client-side only).
181
+ * // Survives back/forward and hard refresh; useLocationState() readers will
182
+ * // NOT see the new value until the next navigation. Pair with .read() or a
183
+ * // fresh mount.
184
+ * ProductState.write({ name: "Widget", price: 9.99 });
185
+ *
186
+ * // Manually clear the slot (non-reactive, client-side only).
187
+ * ProductState.delete();
73
188
  * ```
74
189
  */
75
190
  export function createLocationState<TState>(
76
191
  options?: LocationStateOptions,
77
- ): LocationStateDefinition<[TState | (() => TState)], TState> {
192
+ ): LocationStateDefinition<
193
+ [(TState | (() => TState)) & ValidateLocationState<TState>],
194
+ TState
195
+ > {
78
196
  const flash = options?.flash ?? false;
79
197
  let _key: string | undefined;
80
198
 
81
199
  function getKey(): string {
82
200
  if (!_key && process.env.NODE_ENV === "development") {
83
201
  throw new Error(
84
- "[rsc-router] createLocationState key not set. " +
202
+ "[rango] createLocationState key not set. " +
85
203
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
86
204
  "the state is exported with: export const MyState = createLocationState(...)",
87
205
  );
@@ -128,7 +246,59 @@ export function createLocationState<TState>(
128
246
  enumerable: true,
129
247
  });
130
248
 
131
- return fn as LocationStateDefinition<[TState | (() => TState)], TState>;
249
+ Object.defineProperty(fn, "write", {
250
+ value: (value: TState): void => {
251
+ if (typeof window === "undefined") {
252
+ throw new Error(
253
+ "[rango] LocationState.write() is client-only. " +
254
+ "It mutates window.history.state and cannot run on the server.",
255
+ );
256
+ }
257
+ const key = getKey();
258
+ // history.state may be a non-null primitive (string/number/boolean) if
259
+ // non-Rango code called pushState/replaceState with one. `?? {}` only
260
+ // catches null/undefined, so spreading a primitive would yield indexed
261
+ // char/no keys and corrupt history.state. Coerce any non-object to a fresh
262
+ // dict — mirrors the delete() guard.
263
+ const existing = window.history.state;
264
+ const current =
265
+ existing !== null && typeof existing === "object" ? existing : {};
266
+ window.history.replaceState(
267
+ { ...current, [key]: value },
268
+ "",
269
+ window.location.href,
270
+ );
271
+ },
272
+ enumerable: true,
273
+ });
274
+
275
+ Object.defineProperty(fn, "delete", {
276
+ value: (): void => {
277
+ if (typeof window === "undefined") {
278
+ throw new Error(
279
+ "[rango] LocationState.delete() is client-only. " +
280
+ "It mutates window.history.state and cannot run on the server.",
281
+ );
282
+ }
283
+ const key = getKey();
284
+ const current = window.history.state;
285
+ // history.state may be a non-null primitive (string/number/boolean) if
286
+ // non-Rango code called pushState/replaceState with one. `key in
287
+ // <primitive>` throws, so require an object before the `in` check; a
288
+ // primitive carries no slots, so deletion is a no-op.
289
+ if (current === null || typeof current !== "object" || !(key in current))
290
+ return;
291
+ const next = { ...current };
292
+ delete next[key];
293
+ window.history.replaceState(next, "", window.location.href);
294
+ },
295
+ enumerable: true,
296
+ });
297
+
298
+ return fn as unknown as LocationStateDefinition<
299
+ [(TState | (() => TState)) & ValidateLocationState<TState>],
300
+ TState
301
+ >;
132
302
  }
133
303
 
134
304
  /**
@@ -1,9 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useRef } from "react";
4
4
  import type { LocationStateDefinition } from "./location-state-shared.js";
5
5
 
6
- // Re-export shared utilities and types
7
6
  export {
8
7
  createLocationState,
9
8
  isLocationStateEntry,
@@ -13,6 +12,24 @@ export {
13
12
  type LocationStateOptions,
14
13
  } from "./location-state-shared.js";
15
14
 
15
+ function readLocationStateValue<TState>(
16
+ key: string | undefined,
17
+ ): TState | undefined {
18
+ if (typeof window === "undefined") return undefined;
19
+ if (key) {
20
+ return window.history.state?.[key] as TState | undefined;
21
+ }
22
+ // Plain state: stored under history.state.state
23
+ return window.history.state?.state as TState | undefined;
24
+ }
25
+
26
+ function hasHydrated(): boolean {
27
+ return (
28
+ typeof document !== "undefined" &&
29
+ document.documentElement.hasAttribute("data-hydrated")
30
+ );
31
+ }
32
+
16
33
  /**
17
34
  * Hook to read location state from history.state
18
35
  *
@@ -48,30 +65,33 @@ export function useLocationState<TArgs extends unknown[], TState>(
48
65
  const key = definition?.__rsc_ls_key;
49
66
  const isFlash = definition?.__rsc_ls_flash ?? false;
50
67
 
68
+ // Track whether the initial render returned undefined because the page
69
+ // hadn't hydrated yet. If so, the mount effect catches up by reading
70
+ // history.state once. If not, we already have the right value and must
71
+ // not re-read on mount — under StrictMode, the flash-cleanup effect runs
72
+ // before the second setup pass, so a re-read would clobber the captured
73
+ // value with the now-cleared `undefined`.
74
+ const initialReadDeferredRef = useRef(false);
75
+
51
76
  const [state, setState] = useState<TState | undefined>(() => {
52
- if (typeof window === "undefined") return undefined;
53
- if (key) {
54
- return window.history.state?.[key] as TState | undefined;
77
+ if (!hasHydrated()) {
78
+ initialReadDeferredRef.current = true;
79
+ return undefined;
55
80
  }
56
- // Plain state: stored under history.state.state
57
- return window.history.state?.state as TState | undefined;
81
+ return readLocationStateValue<TState>(key);
58
82
  });
59
83
 
60
84
  // Subscribe to popstate and programmatic state changes
61
85
  useEffect(() => {
62
86
  const handlePopstate = () => {
63
- if (key) {
64
- setState(window.history.state?.[key] as TState | undefined);
65
- } else {
66
- setState(window.history.state?.state as TState | undefined);
67
- }
87
+ setState(readLocationStateValue<TState>(key));
68
88
  };
69
89
 
70
90
  // Handle programmatic state changes (same-page navigation with
71
91
  // ctx.setLocationState where components don't remount)
72
92
  const handleLocationState = () => {
73
93
  if (key) {
74
- const val = window.history.state?.[key] as TState | undefined;
94
+ const val = readLocationStateValue<TState>(key);
75
95
  if (isFlash) {
76
96
  // For flash state, only update if there's a new value
77
97
  if (val !== undefined) {
@@ -81,10 +101,15 @@ export function useLocationState<TArgs extends unknown[], TState>(
81
101
  setState(val);
82
102
  }
83
103
  } else {
84
- setState(window.history.state?.state as TState | undefined);
104
+ setState(readLocationStateValue<TState>(key));
85
105
  }
86
106
  };
87
107
 
108
+ if (initialReadDeferredRef.current) {
109
+ initialReadDeferredRef.current = false;
110
+ setState(readLocationStateValue<TState>(key));
111
+ }
112
+
88
113
  window.addEventListener("popstate", handlePopstate);
89
114
  window.addEventListener("__rsc_locationstate", handleLocationState);
90
115
  return () => {
@@ -24,32 +24,24 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
24
24
  result: null,
25
25
  };
26
26
 
27
- /**
28
- * Normalize action ID - returns the ID as-is
29
- *
30
- * Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
31
- * When using function references, we use the full ID for exact matching.
32
- * When using strings, the event controller supports suffix matching
33
- * (e.g., "addToCart" matches "hash#addToCart").
34
- */
35
- function normalizeActionId(actionId: string): string {
36
- return actionId;
37
- }
38
-
39
27
  /**
40
28
  * Extract action ID from a server action function or string.
41
29
  *
42
30
  * Actions passed as props from server components lose their metadata
43
31
  * during RSC serialization - use a string action name instead.
32
+ *
33
+ * The extracted $$id (e.g. "hash#actionName" or "src/actions.ts#actionName")
34
+ * is returned as-is. Suffix-vs-exact matching against this ID happens
35
+ * downstream in the event controller, not here.
44
36
  */
45
- export function getActionId(action: ServerActionFunction | string): string {
37
+ function getActionId(action: ServerActionFunction | string): string {
46
38
  invariant(
47
39
  typeof action === "function" || typeof action === "string",
48
40
  `useAction: action must be a function or string, got ${typeof action}`,
49
41
  );
50
42
  const actionId = (action as any)?.$$id;
51
43
  if (actionId) {
52
- return normalizeActionId(actionId);
44
+ return actionId;
53
45
  }
54
46
 
55
47
  // If action is a string, use it directly
@@ -162,7 +154,6 @@ export function useAction<T>(
162
154
  });
163
155
  const prevSelected = useRef(baseState);
164
156
  prevSelected.current = baseState;
165
- // useOptimistic allows immediate updates during transitions/actions
166
157
  const [optimisticState, setOptimisticState] = useOptimistic<
167
158
  T | TrackedActionState
168
159
  >(null!);
@@ -9,64 +9,11 @@ import {
9
9
  startTransition,
10
10
  } from "react";
11
11
  import type { Handle } from "../../handle.js";
12
- import { getCollectFn } from "../../handle.js";
12
+ import { collectHandleData } from "../../handle.js";
13
13
  import type { HandleData } from "../types.js";
14
14
  import { NavigationStoreContext } from "./context.js";
15
15
  import { shallowEqual } from "./shallow-equal.js";
16
16
 
17
- /**
18
- * Resolve the collect function for a handle.
19
- * Handle objects are plain { __brand, $$id } - collect is stored in the registry
20
- * (populated when createHandle runs on the client).
21
- */
22
- function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
23
- // Look up collect from the registry (populated when the handle module is imported).
24
- const registered = getCollectFn(handle.$$id);
25
- if (registered) {
26
- return registered as (segments: T[][]) => A;
27
- }
28
-
29
- // Fall back to default flat collect with a dev warning.
30
- if (process.env.NODE_ENV !== "production") {
31
- console.warn(
32
- `[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
33
- `function could not be resolved. Falling back to flat array. ` +
34
- `Import the handle module in a client component to register its collect function.`,
35
- );
36
- }
37
- return ((segments: unknown[][]) => segments.flat()) as unknown as (
38
- segments: T[][],
39
- ) => A;
40
- }
41
-
42
- /**
43
- * Collect handle data from segments and transform to final value.
44
- */
45
- function collectHandle<T, A>(
46
- handle: Handle<T, A>,
47
- data: HandleData,
48
- segmentOrder: string[],
49
- ): A {
50
- const collect = resolveCollect(handle);
51
- const segmentData = data[handle.$$id];
52
-
53
- if (!segmentData) {
54
- return collect([]);
55
- }
56
-
57
- // Build array of segment arrays in parent -> child order
58
- const segmentArrays: T[][] = [];
59
- for (const segmentId of segmentOrder) {
60
- const entries = segmentData[segmentId];
61
- if (entries && entries.length > 0) {
62
- segmentArrays.push(entries as T[]);
63
- }
64
- }
65
-
66
- // Call collect once with all segment data
67
- return collect(segmentArrays);
68
- }
69
-
70
17
  /**
71
18
  * Hook to access collected handle data.
72
19
  *
@@ -85,51 +32,54 @@ function collectHandle<T, A>(
85
32
  * const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
86
33
  * ```
87
34
  */
88
- export function useHandle<T, A>(handle: Handle<T, A>): A;
35
+ export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
89
36
  export function useHandle<T, A, S>(
90
37
  handle: Handle<T, A>,
91
- selector: (data: A) => S,
38
+ selector: (data: Rango.FlightSerialize<A>) => S,
92
39
  ): S;
93
40
  export function useHandle<T, A, S>(
94
41
  handle: Handle<T, A>,
95
- selector?: (data: A) => S,
96
- ): A | S {
42
+ selector?: (data: Rango.FlightSerialize<A>) => S,
43
+ ): Rango.FlightSerialize<A> | S {
97
44
  const ctx = useContext(NavigationStoreContext);
98
45
 
99
- // Initial state from context event controller, or empty fallback without provider.
100
- const [value, setValue] = useState<A | S>(() => {
46
+ const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
101
47
  if (!ctx) {
102
- const collected = collectHandle(handle, {}, []);
48
+ const collected = collectHandleData(
49
+ handle,
50
+ {},
51
+ [],
52
+ ) as Rango.FlightSerialize<A>;
103
53
  return selector ? selector(collected) : collected;
104
54
  }
105
55
 
106
- // On client, use event controller state
107
56
  const state = ctx.eventController.getHandleState();
108
- const collected = collectHandle(handle, state.data, state.segmentOrder);
57
+ const collected = collectHandleData(
58
+ handle,
59
+ state.data,
60
+ state.segmentOrder,
61
+ ) as Rango.FlightSerialize<A>;
109
62
  return selector ? selector(collected) : collected;
110
63
  });
111
64
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
112
65
 
113
- // Track previous value for shallow comparison
114
66
  const prevValueRef = useRef(value);
115
67
  prevValueRef.current = value;
116
68
 
117
- // Ref keeps the latest selector without re-subscribing on every render.
118
69
  const selectorRef = useRef(selector);
119
70
  selectorRef.current = selector;
120
71
 
121
- // Subscribe to handle data changes (client only)
122
72
  useEffect(() => {
123
73
  if (!ctx) return;
124
74
 
125
75
  // Sync current state for the (possibly new) handle so that switching
126
76
  // handles on an idle page doesn't leave stale data from the old handle.
127
77
  const currentHandleState = ctx.eventController.getHandleState();
128
- const currentCollected = collectHandle(
78
+ const currentCollected = collectHandleData(
129
79
  handle,
130
80
  currentHandleState.data,
131
81
  currentHandleState.segmentOrder,
132
- );
82
+ ) as Rango.FlightSerialize<A>;
133
83
  const currentValue = selectorRef.current
134
84
  ? selectorRef.current(currentCollected)
135
85
  : currentCollected;
@@ -142,7 +92,11 @@ export function useHandle<T, A, S>(
142
92
  const state = ctx.eventController.getHandleState();
143
93
  const isAction =
144
94
  ctx.eventController.getState().inflightActions.length > 0;
145
- const collected = collectHandle(handle, state.data, state.segmentOrder);
95
+ const collected = collectHandleData(
96
+ handle,
97
+ state.data,
98
+ state.segmentOrder,
99
+ ) as Rango.FlightSerialize<A>;
146
100
  const nextValue = selectorRef.current
147
101
  ? selectorRef.current(collected)
148
102
  : collected;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useCallback } from "react";
3
4
  import { href, type ValidPaths } from "../../href-client.js";
4
5
  import { useMount } from "./use-mount.js";
5
6
 
@@ -36,5 +37,11 @@ import { useMount } from "./use-mount.js";
36
37
  */
37
38
  export function useHref(): (path: `/${string}`) => string {
38
39
  const mount = useMount();
39
- return (path: `/${string}`) => href(path as ValidPaths, mount);
40
+ // Memoize on `mount` (stable within a route) so the returned function is
41
+ // referentially stable across re-renders — a consumer can safely pass it as a
42
+ // dependency or a prop to a memoized child without forcing re-renders.
43
+ return useCallback(
44
+ (path: `/${string}`) => href(path as ValidPaths, mount),
45
+ [mount],
46
+ );
40
47
  }