@rangojs/router 0.20.0 → 0.22.0

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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -8,9 +8,12 @@ import React, {
8
8
  useMemo,
9
9
  useRef,
10
10
  use,
11
+ startTransition,
11
12
  type ReactNode,
12
13
  } from "react";
13
14
  import {
15
+ HydrationBarrierContext,
16
+ LocationStateContext,
14
17
  NavigationStoreContext,
15
18
  type NavigationStoreContextValue,
16
19
  } from "./context.js";
@@ -36,6 +39,7 @@ import {
36
39
  deferredHandleNames,
37
40
  resolveDeferredHandleValues,
38
41
  } from "../../handles/deferred-resolution.js";
42
+ import { auditTreeUpdate } from "../../suspense-audit.js";
39
43
 
40
44
  /**
41
45
  * Process handles from an async generator, updating the event controller
@@ -277,6 +281,18 @@ export interface NavigationProviderProps {
277
281
  * it to seed a nonce so components calling useNonce() can be exercised.
278
282
  */
279
283
  nonce?: string;
284
+
285
+ /**
286
+ * The document this provider hydrates (initBrowserApp; renderRoute with
287
+ * `hydrate: true`), whose store EventController.lockHydration locked.
288
+ * `settled` resolves once the stream has ended and React has revealed every
289
+ * boundary. Until then location state is `undefined`, as SSR rendered it;
290
+ * then one transition gives the entry's state and changes
291
+ * HydrationBarrierContext, and the store is released after that commit. A
292
+ * navigation committed earlier releases the store before its update.
293
+ * Absent for a provider mounted on the client.
294
+ */
295
+ hydration?: { settled: Promise<void> };
280
296
  }
281
297
 
282
298
  /**
@@ -312,9 +328,18 @@ export function NavigationProvider({
312
328
  basename,
313
329
  appShellRef,
314
330
  nonce,
331
+ hydration,
315
332
  }: NavigationProviderProps): ReactNode {
316
333
  // Track current payload for rendering (this triggers re-renders)
317
334
  const [payload, setPayload] = useState(initialPayload);
335
+ // The location state of the entry `payload` renders (LocationStateContext).
336
+ // Only ever set next to setPayload, or for a commit that has no payload.
337
+ const [locationState, setLocationState] = useState(() =>
338
+ hydration ? undefined : eventController.getLocationState(),
339
+ );
340
+ // HydrationBarrierContext's value; see NavigationProviderProps.hydration.
341
+ const [hydrationBarrier, setHydrationBarrier] = useState(0);
342
+ const hydrationOpen = useRef(hydration !== undefined);
318
343
 
319
344
  /**
320
345
  * Navigate to a URL (delegates to bridge)
@@ -350,6 +375,8 @@ export function NavigationProvider({
350
375
  navigate,
351
376
  refresh,
352
377
  defaultPrefetch: initialPayload.metadata.defaultPrefetch,
378
+ // Fixed for the document: read once, with the stable value.
379
+ origin: (eventController.getState().location as URL | undefined)?.origin,
353
380
  } as NavigationStoreContextValue;
354
381
  Object.defineProperty(value, "basename", {
355
382
  configurable: true,
@@ -373,6 +400,45 @@ export function NavigationProvider({
373
400
  return startConnectionWarmup();
374
401
  }, [warmupEnabled]);
375
402
 
403
+ // The document has been revealed: one transition gives the entry's location
404
+ // state and moves the barrier, so React hydrates every boundary still
405
+ // dehydrated before it commits.
406
+ useEffect(() => {
407
+ if (!hydration) return;
408
+ let cancelled = false;
409
+ void hydration.settled.then(() => {
410
+ if (cancelled || !hydrationOpen.current) return;
411
+ startTransition(() => {
412
+ setLocationState(eventController.getLocationState());
413
+ setHydrationBarrier((n) => n + 1);
414
+ });
415
+ });
416
+ return () => {
417
+ cancelled = true;
418
+ };
419
+ }, []);
420
+
421
+ // After the barrier's commit no boundary under this provider is dehydrated:
422
+ // release the store, and its handle writes reach their readers.
423
+ useEffect(() => {
424
+ if (hydrationBarrier === 0) return;
425
+ hydrationOpen.current = false;
426
+ eventController.releaseHydrationLock();
427
+ }, [hydrationBarrier]);
428
+
429
+ // A commit no payload follows (a shallow navigation, a commit that keeps
430
+ // every segment, merged server-set state): the tree on screen is already
431
+ // the entry's, so its state is taken with the commit's notification.
432
+ useEffect(
433
+ () =>
434
+ eventController.subscribe(() => {
435
+ if (eventController.takeTreelessLocationState()) {
436
+ setLocationState(eventController.getLocationState());
437
+ }
438
+ }),
439
+ [eventController],
440
+ );
441
+
376
442
  // Cancel non-matching prefetches when navigation starts.
377
443
  // Frees connections so the navigation fetch isn't competing with
378
444
  // speculative prefetches. The prefetch matching the navigation target
@@ -411,16 +477,30 @@ export function NavigationProvider({
411
477
  // Subscribe to UI updates (for re-rendering the tree)
412
478
  useEffect(() => {
413
479
  const unsubscribe = store.onUpdate((update) => {
480
+ if (process.env.NODE_ENV !== "production") auditTreeUpdate(update.root);
414
481
  // Capture scroll intent — it will be applied in useLayoutEffect
415
482
  // after React commits this state update to the DOM.
416
483
  // Always assign (even undefined) to clear stale scroll from prior navigations,
417
484
  // so server actions or error updates don't accidentally replay old scroll.
418
485
  pendingScrollRef.current = update.scroll;
419
486
 
487
+ // Committed before the document finished hydrating: end the window
488
+ // first, so the page this update mounts reads the live store. A
489
+ // boundary of the document still dehydrated then hydrates against the
490
+ // live store; where that differs from its HTML, React reports the
491
+ // mismatch and renders the boundary on the client.
492
+ if (hydrationOpen.current) {
493
+ hydrationOpen.current = false;
494
+ eventController.releaseHydrationLock();
495
+ }
496
+
420
497
  setPayload({
421
498
  root: update.root,
422
499
  metadata: update.metadata,
423
500
  });
501
+ // The state of the entry this payload renders, as its commit recorded
502
+ // it: one update, so one lane, so one React commit with the tree.
503
+ setLocationState(eventController.getLocationState());
424
504
 
425
505
  // Update route params. Only reset when the server actually sends a params
426
506
  // map — an absent `params` field means "no change" (e.g., legacy action
@@ -506,7 +586,11 @@ export function NavigationProvider({
506
586
 
507
587
  return (
508
588
  <NavigationStoreContext.Provider value={contextValue}>
509
- {content}
589
+ <LocationStateContext.Provider value={locationState}>
590
+ <HydrationBarrierContext.Provider value={hydrationBarrier}>
591
+ {content}
592
+ </HydrationBarrierContext.Provider>
593
+ </LocationStateContext.Provider>
510
594
  </NavigationStoreContext.Provider>
511
595
  );
512
596
  }
@@ -4,6 +4,7 @@ import { createContext, type Context } from "react";
4
4
  import type { NavigationStore, NavigateOptionsInternal } from "../types.js";
5
5
  import type { EventController } from "../event-controller.js";
6
6
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
7
+ import type { LocationStateSnapshot } from "./location-state-shared.js";
7
8
 
8
9
  /**
9
10
  * Navigation context value provided by NavigationProvider
@@ -56,6 +57,13 @@ export interface NavigationStoreContextValue {
56
57
 
57
58
  /** Router default from this instance's initial payload. */
58
59
  defaultPrefetch?: PrefetchStrategy;
60
+
61
+ /**
62
+ * The document's origin, fixed for its lifetime. Link compares absolute
63
+ * hrefs against it (data-external) and useLinkStatus resolves link targets
64
+ * with it.
65
+ */
66
+ origin: string | undefined;
59
67
  }
60
68
 
61
69
  /**
@@ -66,3 +74,25 @@ export interface NavigationStoreContextValue {
66
74
  */
67
75
  export const NavigationStoreContext: Context<NavigationStoreContextValue | null> =
68
76
  createContext<NavigationStoreContextValue | null>(null);
77
+
78
+ /**
79
+ * The location state of the history entry whose tree is on screen, for
80
+ * useLocationState. NavigationProvider holds it as React state next to the
81
+ * payload and sets both in one update, so a render sees an entry's state
82
+ * exactly when it sees that entry's tree: a transition React holds renders
83
+ * the destination's value, an urgent render of the content still on screen
84
+ * (a reader mounting there included) the value of the entry being left
85
+ * (#1029). Undefined on the server and outside a provider.
86
+ */
87
+ export const LocationStateContext: Context<LocationStateSnapshot> =
88
+ createContext<LocationStateSnapshot>(undefined);
89
+
90
+ /**
91
+ * Read by no component. NavigationProvider changes its value in a transition so
92
+ * that React hydrates every boundary still dehydrated beneath it before that
93
+ * commit: React hydrates a dehydrated boundary first when a provider above it
94
+ * changes, and nothing re-renders for a value nobody reads. See
95
+ * NavigationProviderProps.hydration.
96
+ */
97
+ export const HydrationBarrierContext: Context<number> =
98
+ createContext<number>(0);
@@ -11,12 +11,177 @@ export interface LocationStateEntry {
11
11
  readonly __rsc_ls_lazy?: boolean;
12
12
  }
13
13
 
14
+ /** Prefix of every typed location-state slot in `history.state`. */
15
+ export const LOCATION_STATE_KEY_PREFIX: string = "__rsc_ls_";
16
+
17
+ export function isLocationStateKey(key: string): boolean {
18
+ return key.startsWith(LOCATION_STATE_KEY_PREFIX);
19
+ }
20
+
21
+ /**
22
+ * `clearOnReload` is part of a slot's KEY (`<key>~r`); the value is always
23
+ * stored raw. Slots outlive deploys, so nothing that reads one may depend on a
24
+ * stored format: a definition without the option reads the plain key and never
25
+ * sees such a slot.
26
+ *
27
+ * An injected key ends in `#<ExportName>` (expose-id-utils.ts makeStubId; only
28
+ * identifier exports are keyed) and "~" cannot occur in an identifier, so no
29
+ * file or export name produces a suffix. withLocationStateKey rejects it.
30
+ */
31
+ export const LOCATION_STATE_KEY_SUFFIX_SEPARATOR: string = "~";
32
+ export const CLEAR_ON_RELOAD_KEY_SUFFIX: string = "~r";
33
+
34
+ /**
35
+ * A history entry records the app version its location state (typed slots and
36
+ * plain `state`) was written under, in this one field; a read returns only
37
+ * state recorded under the version this document loaded with. Entries outlive
38
+ * deploys, and state an older release stored would otherwise reach code that
39
+ * no longer expects its shape. An entry without the field counts as another
40
+ * version.
41
+ *
42
+ * Not a slot key (no LOCATION_STATE_KEY_PREFIX): hasLocationState must not
43
+ * count it.
44
+ */
45
+ const LOCATION_STATE_VERSION_KEY = "__rsc_lsv";
46
+
47
+ let documentVersion: string | undefined;
48
+
49
+ /**
50
+ * @internal The version the document loaded under (rsc-router.tsx
51
+ * initBrowserApp). Deliberately not getAppVersion(): a dev HMR bump moves that
52
+ * one inside a running session (bridge.updateVersion), and the state that
53
+ * session wrote must stay readable until the next document load.
54
+ */
55
+ export function setLocationStateVersion(version: string | undefined): void {
56
+ documentVersion = version;
57
+ }
58
+
59
+ /**
60
+ * The state of a `{ state }` snapshot when its location state may be read:
61
+ * an object recorded under the client's version. Without a snapshot, the
62
+ * current entry (`window.history` is a `{ state }`), and nothing on the
63
+ * server.
64
+ */
65
+ export function readableLocationState(
66
+ location: { readonly state: unknown } | undefined = typeof window ===
67
+ "undefined"
68
+ ? undefined
69
+ : window.history,
70
+ ): Record<string, unknown> | undefined {
71
+ const state = location?.state;
72
+ return state !== null &&
73
+ typeof state === "object" &&
74
+ (state as Record<string, unknown>)[LOCATION_STATE_VERSION_KEY] ===
75
+ documentVersion
76
+ ? (state as Record<string, unknown>)
77
+ : undefined;
78
+ }
79
+
80
+ /**
81
+ * One history entry's location state as useLocationState readers receive it:
82
+ * its typed slots and plain `state`, or undefined when it holds none.
83
+ */
84
+ export type LocationStateSnapshot =
85
+ | Readonly<Record<string, unknown>>
86
+ | undefined;
87
+
88
+ /**
89
+ * The snapshot of `entryState`: the state object a commit pushes, restores or
90
+ * merges into its history entry. Only state recorded under the client's
91
+ * version is in it (readableLocationState).
92
+ *
93
+ * The snapshot is a React context value, so identity decides who re-renders.
94
+ * It is `previous` itself when every slot holds the value `previous` holds:
95
+ * an entry without state after another, a primitive carried over, or the
96
+ * object a reader got from `previous` passed back by the navigation. Any
97
+ * other value is new, equal content included: a link clicked twice announces
98
+ * its state twice (a flash message, a scroll target). A new value is a
99
+ * structured clone, as history.state holds it, so a reader never shares an
100
+ * object with the caller of the navigation.
101
+ */
102
+ export function locationStateSnapshot(
103
+ entryState: unknown,
104
+ previous?: LocationStateSnapshot,
105
+ ): LocationStateSnapshot {
106
+ const state = readableLocationState({ state: entryState });
107
+ let next: Record<string, unknown> | undefined;
108
+ let kept = 0;
109
+ for (const key in state) {
110
+ if (key !== "state" && !isLocationStateKey(key)) continue;
111
+ next ??= {};
112
+ const value = state[key];
113
+ if (
114
+ previous &&
115
+ Object.hasOwn(previous, key) &&
116
+ Object.is(previous[key], value)
117
+ ) {
118
+ next[key] = value;
119
+ kept++;
120
+ } else {
121
+ next[key] = structuredClone(value);
122
+ }
123
+ }
124
+ return previous &&
125
+ next &&
126
+ kept === Object.keys(next).length &&
127
+ kept === Object.keys(previous).length
128
+ ? previous
129
+ : next;
130
+ }
131
+
132
+ /**
133
+ * Record the client's version on a state object that carries location state
134
+ * and no record yet. A client without a version (a renderRoute tree) writes
135
+ * none.
136
+ */
137
+ export function stampLocationState(state: Record<string, unknown>): void {
138
+ if (documentVersion !== undefined) {
139
+ state[LOCATION_STATE_VERSION_KEY] = documentVersion;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * `entryState` plus `additions`, recorded under the client's version. The
145
+ * record is per entry, so location state another version left there is dropped
146
+ * first, with its record: under the new one it would be readable again. Router
147
+ * bookkeeping (idx, scroll key, intercept) is kept.
148
+ */
149
+ export function addLocationState(
150
+ entryState: unknown,
151
+ additions?: Record<string, unknown>,
152
+ ): Record<string, unknown> {
153
+ // history.state may be a primitive if non-Rango code stored one; spreading a
154
+ // string would yield indexed char keys.
155
+ const next: Record<string, unknown> = {
156
+ ...(typeof entryState === "object" ? entryState : null),
157
+ };
158
+ if (next[LOCATION_STATE_VERSION_KEY] !== documentVersion) {
159
+ for (const key in next) {
160
+ if (
161
+ key === "state" ||
162
+ key === LOCATION_STATE_VERSION_KEY ||
163
+ isLocationStateKey(key)
164
+ ) {
165
+ delete next[key];
166
+ }
167
+ }
168
+ stampLocationState(next);
169
+ }
170
+ return Object.assign(next, additions);
171
+ }
172
+
14
173
  /**
15
174
  * Options for createLocationState
16
175
  */
17
176
  export interface LocationStateOptions {
18
177
  /** When true, the state is cleared from history after first read (flash message pattern) */
19
178
  flash?: boolean;
179
+ /**
180
+ * When true, the state does not survive a document load of its entry (a
181
+ * refresh, a back/forward that loads the document): the server rendered
182
+ * without it. Not together with `flash`.
183
+ */
184
+ clearOnReload?: boolean;
20
185
  }
21
186
 
22
187
  type LocationStateUnsafeFn = (...args: never[]) => unknown;
@@ -236,13 +401,16 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
236
401
  (...args: TArgs): LocationStateEntry;
237
402
  /**
238
403
  * Injected by Vite plugin - do not set manually. Unit tests without the
239
- * plugin use withLocationStateKey() from @rangojs/router/testing.
404
+ * plugin use withLocationStateKey() from @rangojs/router/testing. Reads back
405
+ * as the slot's key in `history.state`: the injected key plus the
406
+ * `clearOnReload` suffix.
240
407
  */
241
408
  __rsc_ls_key: string;
242
409
  /** Whether this state auto-clears after first read */
243
410
  readonly __rsc_ls_flash: boolean;
244
411
  /**
245
- * Read the current value from history.state.
412
+ * Read the current value from history.state. State the entry holds from
413
+ * another version of the app (an older build) reads as undefined.
246
414
  *
247
415
  * Returns undefined during SSR (no `window`). To stay hydration-safe, do
248
416
  * NOT call read() inline during the initial render — the server returns
@@ -263,13 +431,14 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
263
431
  /**
264
432
  * Statically write the value into the current history entry under this
265
433
  * definition's key, preserving any other keys already on history.state
266
- * (e.g. router bookkeeping, other LocationState slots).
434
+ * (e.g. router bookkeeping, other LocationState slots). Location state
435
+ * another version of the app left on the entry is dropped.
267
436
  *
268
- * This is the non-reactive counterpart to read(): it does not dispatch any
269
- * event, so components reading via useLocationState() will NOT re-render
270
- * until the next navigation/popstate. Use it when you only need the value
271
- * to be there on the next read() or on the next mount (including after
272
- * back/forward and hard refresh of the same entry).
437
+ * This is the non-reactive counterpart to read(): it changes the history
438
+ * entry and nothing a useLocationState() reader shows, whether the reader
439
+ * is mounted now or mounts later. read() returns the value at once; a
440
+ * reader gets it when the entry is next restored: back/forward to it, or
441
+ * (unless the definition sets `clearOnReload`) a hard refresh of it.
273
442
  *
274
443
  * Client-only: throws when called on the server (no history available).
275
444
  */
@@ -279,8 +448,8 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
279
448
  * leaving any other keys on history.state untouched. Idempotent: removing
280
449
  * a slot that isn't present is a no-op.
281
450
  *
282
- * Same non-reactive semantics as write(): no event is dispatched, so
283
- * useLocationState() readers will NOT re-render until the next navigation.
451
+ * Same non-reactive semantics as write(): useLocationState() readers keep
452
+ * the value until the entry is next restored.
284
453
  *
285
454
  * Client-only: throws when called on the server (no history available).
286
455
  */
@@ -321,9 +490,8 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
321
490
  * const snap = ProductState.read();
322
491
  *
323
492
  * // Static write to current history entry (non-reactive, client-side only).
324
- * // Survives back/forward and hard refresh; useLocationState() readers will
325
- * // NOT see the new value until the next navigation. Pair with .read() or a
326
- * // fresh mount.
493
+ * // Survives back/forward and hard refresh; useLocationState() readers see
494
+ * // the new value when the entry is next restored. Pair with .read().
327
495
  * ProductState.write({ name: "Widget", price: 9.99 });
328
496
  *
329
497
  * // Manually clear the slot (non-reactive, client-side only).
@@ -357,6 +525,19 @@ export function createLocationState<TState>(
357
525
  TState
358
526
  > {
359
527
  const flash = options?.flash ?? false;
528
+ const clearOnReload = options?.clearOnReload ?? false;
529
+ // Empty without `clearOnReload`: the injected key is then the storage key,
530
+ // as it was before the option existed.
531
+ const keySuffix = clearOnReload ? CLEAR_ON_RELOAD_KEY_SUFFIX : "";
532
+ // Flash state ends at its first read, so the pair could only drop a message
533
+ // nobody has seen. Folded out of production, like the missing-key check.
534
+ if (process.env.NODE_ENV !== "production" && flash && clearOnReload) {
535
+ throw new Error(
536
+ "[rango] createLocationState: `flash` and `clearOnReload` cannot be combined. " +
537
+ "Flash state is removed at its first read, so `clearOnReload` could only " +
538
+ "drop a flash message that has not been shown yet.",
539
+ );
540
+ }
360
541
  let _key: string | undefined;
361
542
 
362
543
  // Dev and test throw; production folds the check away (the plugin always
@@ -395,7 +576,7 @@ export function createLocationState<TState>(
395
576
  Object.defineProperty(fn, "__rsc_ls_key", {
396
577
  get: () => getKey(),
397
578
  set: (k: string) => {
398
- _key = k;
579
+ _key = k && k + keySuffix;
399
580
  },
400
581
  enumerable: true,
401
582
  configurable: true,
@@ -407,16 +588,8 @@ export function createLocationState<TState>(
407
588
  });
408
589
 
409
590
  Object.defineProperty(fn, "read", {
410
- value: (location?: { readonly state: unknown }): TState | undefined => {
411
- if (location !== undefined) {
412
- const state = location.state;
413
- return state !== null && typeof state === "object"
414
- ? ((state as Record<string, unknown>)[getKey()] as TState | undefined)
415
- : undefined;
416
- }
417
- if (typeof window === "undefined") return undefined;
418
- return window.history.state?.[getKey()] as TState | undefined;
419
- },
591
+ value: (location?: { readonly state: unknown }): TState | undefined =>
592
+ readableLocationState(location)?.[getKey()] as TState | undefined,
420
593
  enumerable: true,
421
594
  });
422
595
 
@@ -428,16 +601,9 @@ export function createLocationState<TState>(
428
601
  "It mutates window.history.state and cannot run on the server.",
429
602
  );
430
603
  }
431
- const key = getKey();
432
- // history.state may be a non-null primitive (string/number/boolean) if
433
- // non-Rango code called pushState/replaceState with one. `?? {}` only
434
- // catches null/undefined, so spreading a primitive would yield indexed
435
- // char/no keys and corrupt history.state. Coerce any non-object to a fresh
436
- // dict — mirrors the delete() guard.
437
- const existing = window.history.state;
438
- const current =
439
- existing !== null && typeof existing === "object" ? existing : {};
440
- replaceCurrentHistoryState({ ...current, [key]: value });
604
+ replaceCurrentHistoryState(
605
+ addLocationState(window.history.state, { [getKey()]: value }),
606
+ );
441
607
  },
442
608
  enumerable: true,
443
609
  });
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useRef } from "react";
3
+ import { useContext, useEffect } from "react";
4
+ import { LocationStateContext } from "./context.js";
5
+ import { OptimisticLocationContext } from "../../client-urls/optimistic-location.js";
4
6
  import {
7
+ readableLocationState,
5
8
  replaceCurrentHistoryState,
6
9
  type LocationStateDefinition,
7
10
  } from "./location-state-shared.js";
@@ -15,29 +18,16 @@ export {
15
18
  type LocationStateOptions,
16
19
  } from "./location-state-shared.js";
17
20
 
18
- function readLocationStateValue<TState>(
19
- key: string | undefined,
20
- ): TState | undefined {
21
- if (typeof window === "undefined") return undefined;
22
- if (key) {
23
- return window.history.state?.[key] as TState | undefined;
24
- }
25
- // Plain state: stored under history.state.state
26
- return window.history.state?.state as TState | undefined;
27
- }
28
-
29
- function hasHydrated(): boolean {
30
- return (
31
- typeof document !== "undefined" &&
32
- document.documentElement.hasAttribute("data-hydrated")
33
- );
34
- }
35
-
36
21
  /**
37
- * Hook to read location state from history.state
22
+ * Hook to read the location state of the history entry on screen
23
+ *
24
+ * A reader sees an entry's state together with that entry's tree: on a
25
+ * navigation the value changes in the React commit that brings the
26
+ * destination, so content a pending navigation keeps on screen keeps the
27
+ * state of the entry being left, for a reader that mounts there too (#1029).
38
28
  *
39
29
  * Behavior depends on the definition:
40
- * - Normal state: persists across navigations, reactive to popstate
30
+ * - Normal state: persists across navigations and back/forward
41
31
  * - Flash state (created with { flash: true }): read once, cleared after paint
42
32
  *
43
33
  * Overloaded:
@@ -67,69 +57,30 @@ export function useLocationState<TArgs extends unknown[], TState>(
67
57
  ): TState | undefined {
68
58
  const key = definition?.__rsc_ls_key;
69
59
  const isFlash = definition?.__rsc_ls_flash ?? false;
60
+ const entry = useContext(LocationStateContext);
61
+ const optimistic = useContext(OptimisticLocationContext);
70
62
 
71
- // Track whether the initial render returned undefined because the page
72
- // hadn't hydrated yet. If so, the mount effect catches up by reading
73
- // history.state once. If not, we already have the right value and must
74
- // not re-read on mount — under StrictMode, the flash-cleanup effect runs
75
- // before the second setup pass, so a re-read would clobber the captured
76
- // value with the now-cleared `undefined`.
77
- const initialReadDeferredRef = useRef(false);
78
-
79
- const [state, setState] = useState<TState | undefined>(() => {
80
- if (!hasHydrated()) {
81
- initialReadDeferredRef.current = true;
82
- return undefined;
83
- }
84
- return readLocationStateValue<TState>(key);
85
- });
86
-
87
- // Subscribe to popstate and programmatic state changes
88
- useEffect(() => {
89
- const handlePopstate = () => {
90
- setState(readLocationStateValue<TState>(key));
91
- };
92
-
93
- // Handle programmatic state changes (same-page navigation with
94
- // ctx.setLocationState where components don't remount)
95
- const handleLocationState = () => {
96
- if (key) {
97
- const val = readLocationStateValue<TState>(key);
98
- if (isFlash) {
99
- // For flash state, only update if there's a new value
100
- if (val !== undefined) {
101
- setState(val);
102
- }
103
- } else {
104
- setState(val);
105
- }
106
- } else {
107
- setState(readLocationStateValue<TState>(key));
108
- }
109
- };
110
-
111
- if (initialReadDeferredRef.current) {
112
- initialReadDeferredRef.current = false;
113
- setState(readLocationStateValue<TState>(key));
114
- }
115
-
116
- window.addEventListener("popstate", handlePopstate);
117
- window.addEventListener("__rsc_locationstate", handleLocationState);
118
- return () => {
119
- window.removeEventListener("popstate", handlePopstate);
120
- window.removeEventListener("__rsc_locationstate", handleLocationState);
121
- };
122
- }, [key, isFlash]);
63
+ // Typed state: the slot under its key. Plain state: the `state` slot.
64
+ // Inside an optimistically rendered clientUrls() destination the provider
65
+ // still holds the entry being left: read the entry the navigation will push.
66
+ // While the document hydrates, the provider holds `undefined`, what SSR
67
+ // rendered, and gives the entry's state once every boundary has hydrated
68
+ // (NavigationProvider's hydration barrier, issue #992).
69
+ const state = (optimistic ? readableLocationState(optimistic) : entry)?.[
70
+ key || "state"
71
+ ] as TState | undefined;
123
72
 
124
- // Flash: clear from history.state after paint so subsequent navigations don't see it.
125
- // Depends on `state` so it re-runs when state is set via the event listener.
73
+ // Flash: removed from history.state after paint, so a reload or a return to
74
+ // the entry does not show it again. The provider's snapshot keeps it, so it
75
+ // stays on screen until the next commit of the entry's state. Not while the
76
+ // value is the optimistic entry's: history.state is another entry's.
126
77
  useEffect(() => {
127
- if (isFlash && key && state !== undefined) {
78
+ if (isFlash && key && !optimistic && state !== undefined) {
128
79
  const cleaned = { ...window.history.state };
129
80
  delete cleaned[key];
130
81
  replaceCurrentHistoryState(cleaned);
131
82
  }
132
- }, [isFlash, key, state]);
83
+ }, [isFlash, key, state, optimistic]);
133
84
 
134
85
  return state;
135
86
  }
@@ -149,7 +149,10 @@ export function useAction<T>(
149
149
  if (!ctx) {
150
150
  return selector ? selector(DEFAULT_ACTION_STATE) : DEFAULT_ACTION_STATE;
151
151
  }
152
- const state = ctx.eventController.getActionState(actionId);
152
+ // The document renders no action in flight.
153
+ const state = ctx.eventController.getHydrationSnapshot()
154
+ ? DEFAULT_ACTION_STATE
155
+ : ctx.eventController.getActionState(actionId);
153
156
  return selector ? selector(state) : state;
154
157
  });
155
158
  const prevSelected = useRef(baseState);
@@ -53,6 +53,9 @@ export function useHandle<T, A, S>(
53
53
  return selector ? selector(collected) : collected;
54
54
  }
55
55
 
56
+ // Until every boundary has hydrated the store returns the state the
57
+ // document's HTML was rendered with (EventController.lockHydration), so a
58
+ // reader hydrating late reads what SSR rendered (issue #1035).
56
59
  const state = ctx.eventController.getHandleState();
57
60
  const collected = collectHandleData(
58
61
  handle,