@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -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/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -12,40 +12,15 @@ import type { Handle } from "../../handle.js";
12
12
  import { getCollectFn } 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
- }
15
+ import { shallowEqual } from "./shallow-equal.js";
35
16
 
36
17
  /**
37
18
  * 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.
19
+ * Handle objects are plain { __brand, $$id } - collect is stored in the registry
20
+ * (populated when createHandle runs on the client).
41
21
  */
42
22
  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).
23
+ // Look up collect from the registry (populated when the handle module is imported).
49
24
  const registered = getCollectFn(handle.$$id);
50
25
  if (registered) {
51
26
  return registered as (segments: T[][]) => A;
@@ -55,11 +30,13 @@ function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
55
30
  if (process.env.NODE_ENV !== "production") {
56
31
  console.warn(
57
32
  `[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.`
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.`,
60
35
  );
61
36
  }
62
- return ((segments: unknown[][]) => segments.flat()) as unknown as (segments: T[][]) => A;
37
+ return ((segments: unknown[][]) => segments.flat()) as unknown as (
38
+ segments: T[][],
39
+ ) => A;
63
40
  }
64
41
 
65
42
  /**
@@ -68,7 +45,7 @@ function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
68
45
  function collectHandle<T, A>(
69
46
  handle: Handle<T, A>,
70
47
  data: HandleData,
71
- segmentOrder: string[]
48
+ segmentOrder: string[],
72
49
  ): A {
73
50
  const collect = resolveCollect(handle);
74
51
  const segmentData = data[handle.$$id];
@@ -90,45 +67,6 @@ function collectHandle<T, A>(
90
67
  return collect(segmentArrays);
91
68
  }
92
69
 
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
- }
131
-
132
70
  /**
133
71
  * Hook to access collected handle data.
134
72
  *
@@ -150,19 +88,18 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
150
88
  export function useHandle<T, A>(handle: Handle<T, A>): A;
151
89
  export function useHandle<T, A, S>(
152
90
  handle: Handle<T, A>,
153
- selector: (data: A) => S
91
+ selector: (data: A) => S,
154
92
  ): S;
155
93
  export function useHandle<T, A, S>(
156
94
  handle: Handle<T, A>,
157
- selector?: (data: A) => S
95
+ selector?: (data: A) => S,
158
96
  ): A | S {
159
97
  const ctx = useContext(NavigationStoreContext);
160
98
 
161
- // Initial state from SSR module state or event controller
99
+ // Initial state from context event controller, or empty fallback without provider.
162
100
  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);
101
+ if (!ctx) {
102
+ const collected = collectHandle(handle, {}, []);
166
103
  return selector ? selector(collected) : collected;
167
104
  }
168
105
 
@@ -177,7 +114,7 @@ export function useHandle<T, A, S>(
177
114
  const prevValueRef = useRef(value);
178
115
  prevValueRef.current = value;
179
116
 
180
- // Memoize selector ref
117
+ // Ref keeps the latest selector without re-subscribing on every render.
181
118
  const selectorRef = useRef(selector);
182
119
  selectorRef.current = selector;
183
120
 
@@ -185,6 +122,22 @@ export function useHandle<T, A, S>(
185
122
  useEffect(() => {
186
123
  if (!ctx) return;
187
124
 
125
+ // Sync current state for the (possibly new) handle so that switching
126
+ // handles on an idle page doesn't leave stale data from the old handle.
127
+ const currentHandleState = ctx.eventController.getHandleState();
128
+ const currentCollected = collectHandle(
129
+ handle,
130
+ currentHandleState.data,
131
+ currentHandleState.segmentOrder,
132
+ );
133
+ const currentValue = selectorRef.current
134
+ ? selectorRef.current(currentCollected)
135
+ : currentCollected;
136
+ if (!shallowEqual(currentValue, prevValueRef.current)) {
137
+ prevValueRef.current = currentValue;
138
+ setValue(currentValue);
139
+ }
140
+
188
141
  return ctx.eventController.subscribeToHandles(() => {
189
142
  const state = ctx.eventController.getHandleState();
190
143
  const isAction =
@@ -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
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useContext, useMemo } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import { prefetchDirect } from "../prefetch/fetch.js";
6
+ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
7
+
8
+ /**
9
+ * Hook to access router actions (push, replace, refresh, prefetch, back, forward).
10
+ *
11
+ * Returns a STABLE reference that never changes, so components using
12
+ * useRouter() do not re-render on navigation state changes.
13
+ * For reactive navigation state, use useNavigation() instead.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const router = useRouter();
18
+ * router.push("/products");
19
+ * router.replace("/login", { scroll: false });
20
+ * router.prefetch("/dashboard");
21
+ * router.back();
22
+ * ```
23
+ */
24
+ export function useRouter(): RouterInstance {
25
+ const ctx = useContext(NavigationStoreContext);
26
+
27
+ if (!ctx) {
28
+ throw new Error("useRouter must be used within NavigationProvider");
29
+ }
30
+
31
+ // Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
32
+ return useMemo<RouterInstance>(
33
+ () => ({
34
+ push(url: string, options?: RouterNavigateOptions): Promise<void> {
35
+ return ctx.navigate(url, { ...options, replace: false });
36
+ },
37
+
38
+ replace(url: string, options?: RouterNavigateOptions): Promise<void> {
39
+ return ctx.navigate(url, { ...options, replace: true });
40
+ },
41
+
42
+ refresh(): Promise<void> {
43
+ return ctx.refresh();
44
+ },
45
+
46
+ prefetch(url: string): void {
47
+ const segmentState = ctx.store?.getSegmentState();
48
+ if (segmentState) {
49
+ prefetchDirect(url, segmentState.currentSegmentIds, ctx.version);
50
+ }
51
+ },
52
+
53
+ back(): void {
54
+ window.history.back();
55
+ },
56
+
57
+ forward(): void {
58
+ window.history.forward();
59
+ },
60
+ }),
61
+ [],
62
+ );
63
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import type { ReadonlyURLSearchParams } from "../types.js";
6
+
7
+ /**
8
+ * Hook to access the current URL search params.
9
+ *
10
+ * Returns a read-only URLSearchParams object from the committed location.
11
+ * Updates when navigation completes, not during pending navigation.
12
+ *
13
+ * Note: During SSR the search params are not available (the server only sends
14
+ * the pathname). The hook returns empty params during SSR and syncs from
15
+ * the browser URL on mount.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const searchParams = useSearchParams();
20
+ * const query = searchParams.get("q"); // "react"
21
+ * const page = searchParams.get("page"); // "2"
22
+ * ```
23
+ */
24
+ export function useSearchParams(): ReadonlyURLSearchParams {
25
+ const ctx = useContext(NavigationStoreContext);
26
+
27
+ // Always initialize with empty URLSearchParams to match SSR output
28
+ // and avoid hydration mismatch. The useEffect below syncs from
29
+ // the real URL after hydration.
30
+ const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
31
+ () => new URLSearchParams(),
32
+ );
33
+
34
+ const prevSearch = useRef("");
35
+
36
+ useEffect(() => {
37
+ if (!ctx) return;
38
+
39
+ const update = () => {
40
+ const location = ctx.eventController.getState().location as URL;
41
+ const nextSearch = location.searchParams.toString();
42
+ if (nextSearch !== prevSearch.current) {
43
+ prevSearch.current = nextSearch;
44
+ // Create a snapshot so callers cannot mutate the source URLSearchParams
45
+ setSearchParams(new URLSearchParams(nextSearch));
46
+ }
47
+ };
48
+
49
+ // Sync on mount (picks up search params from browser URL)
50
+ update();
51
+
52
+ return ctx.eventController.subscribe(update);
53
+ }, []);
54
+
55
+ return searchParams;
56
+ }