@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.70

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  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 +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -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 +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  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 +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +55 -33
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +743 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1373 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +150 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -9,7 +9,8 @@ import {
9
9
  startTransition,
10
10
  } from "react";
11
11
  import { NavigationStoreContext } from "./context.js";
12
- import type { TrackedActionState, ActionLifecycleState } from "../types.js";
12
+ import { shallowEqual } from "./shallow-equal.js";
13
+ import type { TrackedActionState } from "../types.js";
13
14
  import { invariant } from "../../errors.js";
14
15
 
15
16
  /**
@@ -44,7 +45,7 @@ function normalizeActionId(actionId: string): string {
44
45
  export function getActionId(action: ServerActionFunction | string): string {
45
46
  invariant(
46
47
  typeof action === "function" || typeof action === "string",
47
- `useAction: action must be a function or string, got ${typeof action}`
48
+ `useAction: action must be a function or string, got ${typeof action}`,
48
49
  );
49
50
  const actionId = (action as any)?.$$id;
50
51
  if (actionId) {
@@ -72,7 +73,7 @@ Solutions:
72
73
  2. Use the action name as a string:
73
74
  const state = useAction("myAction");
74
75
 
75
- The string must match the exported function name from your "use server" file.`
76
+ The string must match the exported function name from your "use server" file.`,
76
77
  );
77
78
  }
78
79
 
@@ -126,19 +127,24 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
126
127
  * const error = useAction(addToCart, state => state.error);
127
128
  * ```
128
129
  *
130
+ * @note The selector is expected to be stable for a given hook instance.
131
+ * This hook tracks one projection of one action. Changing selector semantics
132
+ * for the same action ID without a new action event is not a supported pattern;
133
+ * use separate useAction() subscriptions if you need different projections.
134
+ *
129
135
  * @note Actions passed as props from server components lose their metadata
130
136
  * during RSC serialization. Use a string action name or import directly.
131
137
  */
132
138
  export function useAction(
133
- action: ServerActionFunction | string
139
+ action: ServerActionFunction | string,
134
140
  ): TrackedActionState;
135
141
  export function useAction<T>(
136
142
  action: ServerActionFunction | string,
137
- selector: (state: TrackedActionState) => T
143
+ selector: (state: TrackedActionState) => T,
138
144
  ): T;
139
145
  export function useAction<T>(
140
146
  action: ServerActionFunction | string,
141
- selector?: (state: TrackedActionState) => T
147
+ selector?: (state: TrackedActionState) => T,
142
148
  ): T | TrackedActionState {
143
149
  const ctx = useContext(NavigationStoreContext);
144
150
  const actionId =
@@ -161,7 +167,10 @@ export function useAction<T>(
161
167
  T | TrackedActionState
162
168
  >(null!);
163
169
 
164
- // Memoize the selector to avoid unnecessary re-subscriptions
170
+ // Ref keeps the latest selector for subscription callbacks without
171
+ // re-subscribing on every render. Selector changes themselves are not
172
+ // treated as a reactive input; this hook expects a stable selector and
173
+ // represents one subscription/projection for one action.
165
174
  const selectorRef = useRef(selector);
166
175
  selectorRef.current = selector;
167
176
 
@@ -169,6 +178,17 @@ export function useAction<T>(
169
178
  useEffect(() => {
170
179
  if (!ctx) return;
171
180
 
181
+ // Sync current state for the (possibly new) actionId so that switching
182
+ // actions on an idle page doesn't leave stale data from the old action.
183
+ const currentState = ctx.eventController.getActionState(actionId);
184
+ const currentSelected = selectorRef.current
185
+ ? selectorRef.current(currentState)
186
+ : currentState;
187
+ if (!shallowEqual(currentSelected, prevSelected.current)) {
188
+ prevSelected.current = currentSelected;
189
+ setBaseState(currentSelected);
190
+ }
191
+
172
192
  // Subscribe to action-specific updates
173
193
  const unsubscribe = ctx.eventController.subscribeToAction(
174
194
  actionId,
@@ -177,14 +197,14 @@ export function useAction<T>(
177
197
  ? selectorRef.current(state)
178
198
  : state;
179
199
 
180
- if (!isShallowEqual(selectedState, prevSelected.current)) {
200
+ if (!shallowEqual(selectedState, prevSelected.current)) {
181
201
  prevSelected.current = selectedState;
182
202
  setBaseState(selectedState);
183
203
  startTransition(() => {
184
204
  setOptimisticState(selectedState);
185
205
  });
186
206
  }
187
- }
207
+ },
188
208
  );
189
209
 
190
210
  return () => {
@@ -195,46 +215,4 @@ export function useAction<T>(
195
215
  return (optimisticState ?? baseState) as T | TrackedActionState;
196
216
  }
197
217
 
198
- function isShallowEqual<T, U>(selectedState: T, baseState: U): boolean {
199
- // If references are equal, they're shallow equal
200
- //@ts-expect-error -- TS doesn't like comparing generics
201
- if (selectedState === baseState) {
202
- return true;
203
- }
204
-
205
- // If either is null/undefined and they're not equal, they're not shallow equal
206
- if (selectedState == null || baseState == null) {
207
- return false;
208
- }
209
-
210
- // If types are different, they're not shallow equal
211
- if (typeof selectedState !== typeof baseState) {
212
- return false;
213
- }
214
-
215
- // For primitives, === comparison is sufficient (already checked above)
216
- if (typeof selectedState !== "object") {
217
- return false;
218
- }
219
-
220
- // For objects, compare keys and values shallowly
221
- const keysA = Object.keys(selectedState as object);
222
- const keysB = Object.keys(baseState as object);
223
-
224
- if (keysA.length !== keysB.length) {
225
- return false;
226
- }
227
-
228
- for (const key of keysA) {
229
- if (
230
- !Object.prototype.hasOwnProperty.call(baseState, key) ||
231
- (selectedState as any)[key] !== (baseState as any)[key]
232
- ) {
233
- return false;
234
- }
235
- }
236
-
237
- return true;
238
- }
239
-
240
218
  export type { TrackedActionState };
@@ -46,10 +46,12 @@ export interface ClientCacheControls {
46
46
  export function useClientCache(): ClientCacheControls {
47
47
  const ctx = useContext(NavigationStoreContext);
48
48
 
49
+ if (!ctx) {
50
+ throw new Error("useClientCache must be used within NavigationProvider");
51
+ }
52
+
49
53
  const clear = useCallback(() => {
50
- if (ctx?.store) {
51
- ctx.store.clearHistoryCache();
52
- }
54
+ ctx.store.clearHistoryCache();
53
55
  }, [ctx]);
54
56
 
55
57
  return { clear };
@@ -9,125 +9,10 @@ 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
-
16
- /**
17
- * SSR module-level state.
18
- * Populated by initHandleDataSync before React renders.
19
- * Used by useState initializer during SSR.
20
- */
21
- let ssrHandleData: HandleData = {};
22
- let ssrSegmentOrder: string[] = [];
23
-
24
- /**
25
- * Filter segment IDs to only include routes and layouts.
26
- * Excludes parallels (contain .@) and loaders (contain D followed by digit).
27
- */
28
- function filterSegmentOrder(matched: string[]): string[] {
29
- return matched.filter((id) => {
30
- if (id.includes(".@")) return false;
31
- if (/D\d+\./.test(id)) return false;
32
- return true;
33
- });
34
- }
35
-
36
- /**
37
- * Resolve the collect function for a handle.
38
- * When a handle is passed as a prop via RSC, toJSON strips the collect function.
39
- * In that case, look up collect from the registry (populated when createHandle runs
40
- * on the client), then fall back to flat array default.
41
- */
42
- function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
43
- if (typeof handle.collect === "function") {
44
- return handle.collect;
45
- }
46
-
47
- // Handle was deserialized from RSC prop (toJSON stripped collect).
48
- // Try the registry first (populated if the handle module was imported on client).
49
- const registered = getCollectFn(handle.$$id);
50
- if (registered) {
51
- return registered as (segments: T[][]) => A;
52
- }
53
-
54
- // Fall back to default flat collect with a dev warning.
55
- if (process.env.NODE_ENV !== "production") {
56
- console.warn(
57
- `[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
58
- `function could not be resolved. Falling back to flat array. ` +
59
- `Import the handle module in a client component to register its collect function.`
60
- );
61
- }
62
- return ((segments: unknown[][]) => segments.flat()) as unknown as (segments: T[][]) => A;
63
- }
64
-
65
- /**
66
- * Collect handle data from segments and transform to final value.
67
- */
68
- function collectHandle<T, A>(
69
- handle: Handle<T, A>,
70
- data: HandleData,
71
- segmentOrder: string[]
72
- ): A {
73
- const collect = resolveCollect(handle);
74
- const segmentData = data[handle.$$id];
75
-
76
- if (!segmentData) {
77
- return collect([]);
78
- }
79
-
80
- // Build array of segment arrays in parent -> child order
81
- const segmentArrays: T[][] = [];
82
- for (const segmentId of segmentOrder) {
83
- const entries = segmentData[segmentId];
84
- if (entries && entries.length > 0) {
85
- segmentArrays.push(entries as T[]);
86
- }
87
- }
88
-
89
- // Call collect once with all segment data
90
- return collect(segmentArrays);
91
- }
92
-
93
- /**
94
- * Shallow equality check for selector results.
95
- */
96
- function shallowEqual<T>(a: T, b: T): boolean {
97
- if (Object.is(a, b)) return true;
98
- if (
99
- typeof a !== "object" ||
100
- a === null ||
101
- typeof b !== "object" ||
102
- b === null
103
- ) {
104
- return false;
105
- }
106
- const keysA = Object.keys(a);
107
- const keysB = Object.keys(b);
108
- if (keysA.length !== keysB.length) return false;
109
- for (const key of keysA) {
110
- if (
111
- !Object.hasOwn(b, key) ||
112
- !Object.is((a as any)[key], (b as any)[key])
113
- ) {
114
- return false;
115
- }
116
- }
117
- return true;
118
- }
119
-
120
- /**
121
- * Initialize handle data synchronously for SSR.
122
- * Called before rendering to populate state for useState initializer.
123
- *
124
- * @param data - Handle data from RSC payload
125
- * @param matched - Segment order for reduction
126
- */
127
- export function initHandleDataSync(data: HandleData, matched?: string[]): void {
128
- ssrHandleData = data;
129
- ssrSegmentOrder = filterSegmentOrder(matched ?? []);
130
- }
15
+ import { shallowEqual } from "./shallow-equal.js";
131
16
 
132
17
  /**
133
18
  * Hook to access collected handle data.
@@ -150,25 +35,24 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
150
35
  export function useHandle<T, A>(handle: Handle<T, A>): A;
151
36
  export function useHandle<T, A, S>(
152
37
  handle: Handle<T, A>,
153
- selector: (data: A) => S
38
+ selector: (data: A) => S,
154
39
  ): S;
155
40
  export function useHandle<T, A, S>(
156
41
  handle: Handle<T, A>,
157
- selector?: (data: A) => S
42
+ selector?: (data: A) => S,
158
43
  ): A | S {
159
44
  const ctx = useContext(NavigationStoreContext);
160
45
 
161
- // Initial state from SSR module state or event controller
46
+ // Initial state from context event controller, or empty fallback without provider.
162
47
  const [value, setValue] = useState<A | S>(() => {
163
- // During SSR, use module-level state
164
- if (typeof document === "undefined" || !ctx) {
165
- const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
48
+ if (!ctx) {
49
+ const collected = collectHandleData(handle, {}, []);
166
50
  return selector ? selector(collected) : collected;
167
51
  }
168
52
 
169
53
  // On client, use event controller state
170
54
  const state = ctx.eventController.getHandleState();
171
- const collected = collectHandle(handle, state.data, state.segmentOrder);
55
+ const collected = collectHandleData(handle, state.data, state.segmentOrder);
172
56
  return selector ? selector(collected) : collected;
173
57
  });
174
58
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
@@ -177,7 +61,7 @@ export function useHandle<T, A, S>(
177
61
  const prevValueRef = useRef(value);
178
62
  prevValueRef.current = value;
179
63
 
180
- // Memoize selector ref
64
+ // Ref keeps the latest selector without re-subscribing on every render.
181
65
  const selectorRef = useRef(selector);
182
66
  selectorRef.current = selector;
183
67
 
@@ -185,11 +69,31 @@ export function useHandle<T, A, S>(
185
69
  useEffect(() => {
186
70
  if (!ctx) return;
187
71
 
72
+ // Sync current state for the (possibly new) handle so that switching
73
+ // handles on an idle page doesn't leave stale data from the old handle.
74
+ const currentHandleState = ctx.eventController.getHandleState();
75
+ const currentCollected = collectHandleData(
76
+ handle,
77
+ currentHandleState.data,
78
+ currentHandleState.segmentOrder,
79
+ );
80
+ const currentValue = selectorRef.current
81
+ ? selectorRef.current(currentCollected)
82
+ : currentCollected;
83
+ if (!shallowEqual(currentValue, prevValueRef.current)) {
84
+ prevValueRef.current = currentValue;
85
+ setValue(currentValue);
86
+ }
87
+
188
88
  return ctx.eventController.subscribeToHandles(() => {
189
89
  const state = ctx.eventController.getHandleState();
190
90
  const isAction =
191
91
  ctx.eventController.getState().inflightActions.length > 0;
192
- const collected = collectHandle(handle, state.data, state.segmentOrder);
92
+ const collected = collectHandleData(
93
+ handle,
94
+ state.data,
95
+ state.segmentOrder,
96
+ );
193
97
  const nextValue = selectorRef.current
194
98
  ? selectorRef.current(collected)
195
99
  : collected;
@@ -34,7 +34,7 @@ import { useMount } from "./use-mount.js";
34
34
  * }
35
35
  * ```
36
36
  */
37
- export function useHref(): (path: ValidPaths) => string {
37
+ export function useHref(): (path: `/${string}`) => string {
38
38
  const mount = useMount();
39
- return (path: ValidPaths) => href(path, mount);
39
+ return (path: `/${string}`) => href(path as ValidPaths, mount);
40
40
  }
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
16
16
  * Context for Link component to provide its destination URL
17
17
  * Used by useLinkStatus to determine if this specific link is pending
18
18
  */
19
- export const LinkContext: Context<string | null> = createContext<string | null>(null);
19
+ export const LinkContext: Context<string | null> = createContext<string | null>(
20
+ null,
21
+ );
20
22
 
21
23
  /**
22
24
  * Link status returned by useLinkStatus hook
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
46
48
  function isPendingFor(
47
49
  linkTo: string | null,
48
50
  pendingUrl: string | null,
49
- origin: string
51
+ origin: string,
50
52
  ): boolean {
51
53
  if (linkTo === null || pendingUrl === null) {
52
54
  return false;
@@ -81,9 +83,8 @@ export function useLinkStatus(): LinkStatus {
81
83
  const ctx = useContext(NavigationStoreContext);
82
84
 
83
85
  // Get origin for URL normalization (stable across renders)
84
- const origin = typeof window !== "undefined"
85
- ? window.location.origin
86
- : "http://localhost";
86
+ const origin =
87
+ typeof window !== "undefined" ? window.location.origin : "http://localhost";
87
88
 
88
89
  // Base state for useOptimistic
89
90
  const [basePending, setBasePending] = useState<boolean>(() => {
@@ -9,36 +9,10 @@ import {
9
9
  useRef,
10
10
  } from "react";
11
11
  import { NavigationStoreContext } from "./context.js";
12
- import type { PublicNavigationState, NavigateOptions } from "../types.js";
12
+ import { shallowEqual } from "./shallow-equal.js";
13
+ import type { PublicNavigationState } from "../types.js";
13
14
  import type { DerivedNavigationState } from "../event-controller.js";
14
15
 
15
- /**
16
- * Shallow equality check for selector results
17
- */
18
- function shallowEqual<T>(a: T, b: T): boolean {
19
- if (Object.is(a, b)) return true;
20
- if (
21
- typeof a !== "object" ||
22
- a === null ||
23
- typeof b !== "object" ||
24
- b === null
25
- ) {
26
- return false;
27
- }
28
- const keysA = Object.keys(a);
29
- const keysB = Object.keys(b);
30
- if (keysA.length !== keysB.length) return false;
31
- for (const key of keysA) {
32
- if (
33
- !Object.hasOwn(b, key) ||
34
- !Object.is((a as any)[key], (b as any)[key])
35
- ) {
36
- return false;
37
- }
38
- }
39
- return true;
40
- }
41
-
42
16
  /**
43
17
  * Convert derived state to public version (strips inflightActions)
44
18
  */
@@ -47,45 +21,29 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
47
21
  return publicState;
48
22
  }
49
23
 
50
-
51
24
  /**
52
- * Navigation methods returned by useNavigation
53
- */
54
- export interface NavigationMethods {
55
- navigate: (url: string, options?: NavigateOptions) => Promise<void>;
56
- refresh: () => Promise<void>;
57
- }
58
-
59
- /**
60
- * Full value returned when no selector is provided
61
- */
62
- export type NavigationValue = PublicNavigationState & NavigationMethods;
63
-
64
- /**
65
- * Hook to access navigation state with optional selector for performance
25
+ * Hook to access reactive navigation state with optional selector for performance.
66
26
  *
67
- * Uses the event controller for reactive state management.
68
- * State is derived from source of truth (currentNavigation, inflightActions).
27
+ * Returns state only. For actions (push, replace, refresh, prefetch),
28
+ * use useRouter() instead.
69
29
  *
70
30
  * @example
71
31
  * ```tsx
72
- * const state = useNavigation(nav => nav.state);
32
+ * const { state, location } = useNavigation();
73
33
  * const isLoading = useNavigation(nav => nav.state === 'loading');
74
34
  * ```
75
35
  */
76
- export function useNavigation(): NavigationValue;
36
+ export function useNavigation(): PublicNavigationState;
77
37
  export function useNavigation<T>(
78
38
  selector: (state: PublicNavigationState) => T,
79
39
  ): T;
80
40
  export function useNavigation<T>(
81
41
  selector?: (state: PublicNavigationState) => T,
82
- ): T | NavigationValue {
42
+ ): T | PublicNavigationState {
83
43
  const ctx = useContext(NavigationStoreContext);
84
44
 
85
45
  if (!ctx) {
86
- throw new Error(
87
- "useNavigation must be used within NavigationStoreContext.Provider"
88
- );
46
+ throw new Error("useNavigation must be used within NavigationProvider");
89
47
  }
90
48
 
91
49
  // Base state for useOptimistic
@@ -98,13 +56,23 @@ export function useNavigation<T>(
98
56
  // useOptimistic allows immediate updates during transitions/actions
99
57
  const [value, setOptimisticValue] = useOptimistic(baseValue);
100
58
 
59
+ // Store selector in a ref so the subscription callback always uses the
60
+ // latest selector without re-subscribing on every render (inline functions
61
+ // have a new identity each render). This is event-driven by design: the
62
+ // value updates when the store emits, not when the selector changes.
63
+ // Between events there is nothing new to select from.
64
+ const selectorRef = useRef(selector);
65
+ selectorRef.current = selector;
66
+
101
67
  // Subscribe to event controller state changes (only runs on client)
102
68
  useEffect(() => {
103
69
  // Subscribe to updates from event controller
104
70
  return ctx.eventController.subscribe(() => {
105
71
  const currentState = ctx.eventController.getState();
106
72
  const publicState = toPublicState(currentState);
107
- const nextSelected = selector ? selector(publicState) : publicState;
73
+ const nextSelected = selectorRef.current
74
+ ? selectorRef.current(publicState)
75
+ : publicState;
108
76
 
109
77
  // Check if selected value has changed
110
78
  if (!shallowEqual(nextSelected, prevState.current)) {
@@ -125,16 +93,7 @@ export function useNavigation<T>(
125
93
  setBaseValue(nextSelected);
126
94
  }
127
95
  });
128
- }, [selector]);
129
-
130
- // If no selector, include navigation methods
131
- if (!selector) {
132
- return {
133
- ...(value as PublicNavigationState),
134
- navigate: ctx.navigate,
135
- refresh: ctx.refresh,
136
- };
137
- }
96
+ }, []);
138
97
 
139
- return value as T;
98
+ return value as T | PublicNavigationState;
140
99
  }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import { shallowEqual } from "./shallow-equal.js";
6
+
7
+ /**
8
+ * Hook to access the current route params.
9
+ *
10
+ * Returns the merged route params from the matched route.
11
+ * Updates when navigation completes, not during pending navigation.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * // Route: /products/:productId
16
+ * const params = useParams();
17
+ * // { productId: "123" }
18
+ *
19
+ * // With selector
20
+ * const productId = useParams(p => p.productId);
21
+ * ```
22
+ */
23
+ export function useParams(): Record<string, string>;
24
+ export function useParams<T>(
25
+ selector: (params: Record<string, string>) => T,
26
+ ): T;
27
+ export function useParams<T>(
28
+ selector?: (params: Record<string, string>) => T,
29
+ ): T | Record<string, string> {
30
+ const ctx = useContext(NavigationStoreContext);
31
+
32
+ const [value, setValue] = useState<T | Record<string, string>>(() => {
33
+ if (!ctx) {
34
+ return selector ? selector({}) : {};
35
+ }
36
+ const params = ctx.eventController.getParams();
37
+ return selector ? selector(params) : params;
38
+ });
39
+
40
+ const prevValue = useRef(value);
41
+ // Ref keeps the latest selector without re-subscribing. Event-driven by
42
+ // design: value updates on store events, not on selector identity change.
43
+ const selectorRef = useRef(selector);
44
+ selectorRef.current = selector;
45
+
46
+ useEffect(() => {
47
+ if (!ctx) return;
48
+
49
+ const update = () => {
50
+ const params = ctx.eventController.getParams();
51
+ const next = selectorRef.current ? selectorRef.current(params) : params;
52
+
53
+ if (!shallowEqual(next, prevValue.current)) {
54
+ prevValue.current = next;
55
+ setValue(next);
56
+ }
57
+ };
58
+
59
+ update();
60
+
61
+ return ctx.eventController.subscribe(update);
62
+ }, []);
63
+
64
+ return value;
65
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+
6
+ /**
7
+ * Hook to access the current pathname.
8
+ *
9
+ * Returns the committed pathname string (excludes search params and hash).
10
+ * Updates when navigation completes, not during pending navigation.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const pathname = usePathname();
15
+ * // "/products/123"
16
+ * ```
17
+ */
18
+ export function usePathname(): string {
19
+ const ctx = useContext(NavigationStoreContext);
20
+
21
+ const [pathname, setPathname] = useState<string>(() => {
22
+ if (!ctx) {
23
+ return "/";
24
+ }
25
+ return (ctx.eventController.getState().location as URL).pathname;
26
+ });
27
+
28
+ const prevPathname = useRef(pathname);
29
+
30
+ useEffect(() => {
31
+ if (!ctx) return;
32
+
33
+ const update = () => {
34
+ const next = (ctx.eventController.getState().location as URL).pathname;
35
+ if (next !== prevPathname.current) {
36
+ prevPathname.current = next;
37
+ setPathname(next);
38
+ }
39
+ };
40
+
41
+ update();
42
+
43
+ return ctx.eventController.subscribe(update);
44
+ }, []);
45
+
46
+ return pathname;
47
+ }