@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
@@ -20,11 +20,12 @@ To choose between the in-app layers themselves, start at `/cache-guide`.
20
20
  | `"use cache"` | one function result | yes | yes | caller, handlers, loaders, rendering |
21
21
  | `cache()` | serialized Flight segments | yes | yes | middleware, loaders, HTML rendering |
22
22
  | `Prerender()` | build-time Flight segments in the server bundle | yes | yes | middleware, loaders, HTML rendering |
23
+ | `Prerender(..., { onDemand })` | refreshed Flight segments in a writable store | yes | yes | middleware, loaders, HTML rendering |
23
24
  | `ppr` | HTML prelude + React postponed state | yes | **yes, before shell commit** | middleware, holes, hydration payload |
24
25
  | `createDocumentCacheMiddleware()` | complete response in the app cache store | yes | outer middleware runs; the hit skips its downstream pipeline | nothing downstream |
25
26
  | HTTP CDN cache (`s-maxage`) | complete HTTP response outside the app | **no** | **no** | nothing |
26
27
 
27
- The first five rows enter the app. The first four preserve the complete request
28
+ The first six rows enter the app. The first five preserve the complete request
28
29
  model; a document-cache middleware hit intentionally short-circuits its
29
30
  downstream pipeline. The last row is a platform cache: on a hit it serves bytes
30
31
  without invoking Rango at all.
@@ -71,12 +72,31 @@ Worker at the edge, middleware runs there, and PPR shell lookup/resume stays in
71
72
  that Worker. `CFCacheStore` supplies the app cache families; client assets are
72
73
  served separately as assets.
73
74
 
75
+ An on-demand prerender store (`/prerender` → "On-demand refresh") on Workers
76
+ is `createKVPrerenderStore(env.PRERENDER_KV)` from
77
+ `@rangojs/router/prerender/cloudflare`: refreshes run from `scheduled`,
78
+ `queue` or a webhook route, and every request to an on-demand route pays one
79
+ KV read plus one per tag on the entry.
80
+
81
+ The same `router.prerender()` call warms any other route's caches before
82
+ traffic, when `CFCacheStore` has `kv` (shared by every colo). It fills the
83
+ calling colo and KV; a colo that already holds its own copy keeps it until it
84
+ expires, so a content refresh is `updateTag()` and then the warm: no colo
85
+ serves the old copy, and a colo without one reads the warmed entry
86
+ (`/cloudflare` → "Warm routes on Workers").
87
+
74
88
  ### Vercel
75
89
 
76
90
  The Vercel preset emits static client assets plus one streaming Node Function.
77
91
  HTML, Flight, prerender payloads, and PPR shells are served from that function.
78
92
  `VercelCacheStore` uses Runtime Cache inside the function; Runtime Cache is not
79
- Vercel's CDN/ISR cache.
93
+ Vercel's CDN/ISR cache. Rango's on-demand prerender does not use Vercel ISR
94
+ either: it needs a durable `WritablePrerenderStore` shared by every function
95
+ instance, and v1 ships none for Vercel (the in-memory store is per process),
96
+ so a Vercel app writes a small get/set adapter over its own storage.
97
+ Warming a plain route (`/prerender` → "Warm any route before traffic") needs
98
+ no such store: it writes to `VercelCacheStore`, in the region the call runs
99
+ in.
80
100
 
81
101
  The preset deliberately emits no Vercel `.prerender-config.json`, response
82
102
  `chain`, or CDN-stitched resume function. Vercel's open-source Build Output
@@ -155,15 +175,16 @@ the app store, should own the complete response.
155
175
 
156
176
  ## Decision guide
157
177
 
158
- | Requirement | Choose |
159
- | --------------------------------------------------- | ------------------------------------------ |
160
- | Per-request auth or request shaping | in-function caching; never shared CDN HTML |
161
- | Stable shell with cart/session/live prices | `ppr` with live holes |
162
- | Build-known segments with live loaders | `Prerender()` |
163
- | Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
164
- | Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
165
- | One query or component is expensive | `"use cache"` |
166
- | One route subtree is expensive | `cache()` |
178
+ | Requirement | Choose |
179
+ | --------------------------------------------------- | ----------------------------------------------------- |
180
+ | Per-request auth or request shaping | in-function caching; never shared CDN HTML |
181
+ | Stable shell with cart/session/live prices | `ppr` with live holes |
182
+ | Build-known segments with live loaders | `Prerender()` |
183
+ | Prerendered page refreshed on a CMS edit, no deploy | `Prerender(..., { onDemand })` + `router.prerender()` |
184
+ | Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
185
+ | Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
186
+ | One query or component is expensive | `"use cache"` |
187
+ | One route subtree is expensive | `cache()` |
167
188
 
168
189
  For a large dynamic app, CDN-stitched PPR would mainly improve shell first-byte
169
190
  latency and avoid sending the prelude from the function. It would not remove the
@@ -175,7 +196,7 @@ provably public and shared.
175
196
  ## Related skills
176
197
 
177
198
  - `/ppr` — shell capture, holes, middleware commit point, invalidation
178
- - `/prerender` — build-time Flight segments and `Prerender + ppr`
199
+ - `/prerender` — build-time Flight segments, `Prerender + ppr`, and on-demand refresh
179
200
  - `/document-cache` — store-backed complete-response middleware
180
201
  - `/vercel` — Vercel Build Output preset and Runtime Cache wiring
181
202
  - `/cloudflare` — Worker deployment and bindings
@@ -175,20 +175,22 @@ header or logs to identify an actual CDN hit.
175
175
 
176
176
  ## Cache Key Generation
177
177
 
178
- The default key is the host, pathname, and sorted search string (internal
179
- `_rsc*` params removed, `cache.searchParams` filtering applied), followed by
180
- suffixes that separate response variants:
178
+ The default key is the serving router's id, the host, pathname, and sorted
179
+ search string (internal `_rsc*` params removed, `cache.searchParams` filtering
180
+ applied), followed by suffixes that separate response variants:
181
181
 
182
- - HTML document requests: `{host}{pathname}[?search]:html`
182
+ - HTML document requests: `{routerId}@{host}{pathname}[?search]:html`
183
183
  - RSC requests (client navigations and full-document Flight fetches):
184
- `{host}{pathname}[?search]:rsc`
184
+ `{routerId}@{host}{pathname}[?search]:rsc`
185
185
  - Client navigations additionally add a hash of the segments the client already
186
186
  has (`:{segmentHash}`), so navigations from pages with different layouts get
187
187
  different cached payloads, and a `:fragments` marker when the client accepts
188
188
  fragment envelopes.
189
189
 
190
- A custom `keyGenerator(url)` replaces only the `{host}{pathname}[?search]` part;
191
- the variant suffixes are still appended.
190
+ A custom `keyGenerator(url)` replaces the whole
191
+ `{routerId}@{host}{pathname}[?search]` part, router id included, so a generator
192
+ used by routers that share a store must keep them apart itself; the variant
193
+ suffixes are still appended.
192
194
 
193
195
  ## What Gets Cached
194
196
 
@@ -320,13 +322,13 @@ everything else.
320
322
 
321
323
  ## Document Cache vs Segment Cache
322
324
 
323
- | Feature | Document Cache | Segment Cache (`cache()`) |
324
- | ------------ | ------------------------------------- | --------------------------------- |
325
- | Granularity | Full response | Individual segments |
326
- | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
327
- | Loaders | Frozen with the response | Always run fresh |
328
- | Use case | Static pages | Dynamic compositions |
329
- | Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
325
+ | Feature | Document Cache | Segment Cache (`cache()`) |
326
+ | ------------ | ---------------------------------------------- | ------------------------------------------ |
327
+ | Granularity | Full response | Individual segments |
328
+ | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
329
+ | Loaders | Frozen with the response | Always run fresh |
330
+ | Use case | Static pages | Dynamic compositions |
331
+ | Key includes | router + host + URL + variant (+ segment hash) | router + host + pathname + params + search |
330
332
 
331
333
  Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
332
334
 
@@ -68,6 +68,9 @@ the one for your case.
68
68
  - [`./state.md`](./state.md) — `useLocationState` (persistent + flash state,
69
69
  `.read()`/`.read(location)`/`.write()`/`.delete()`, typed `state` on `router.push()` /
70
70
  `router.replace()`, serializability errors that name the failing field),
71
+ state from another deploy (dropped by the router, no version to manage), the
72
+ `createLocationState` options (`flash`; `clearOnReload` for state that must
73
+ not come back after a refresh, such as a "load more" list),
71
74
  `invalidateClientCache()`.
72
75
  - [`./outlets.md`](./outlets.md) — `Outlet`, `ParallelOutlet`, `useOutlet`.
73
76
  - [`./urls.md`](./urls.md) — `useParams`, `usePathname`, `useSearchParams`,
@@ -94,3 +97,25 @@ the one for your case.
94
97
  | `useAction()` | Server action state | state, actionId, payload, error, result |
95
98
  | `useLocationState()` | History state (persists or flash) | T \| undefined |
96
99
  | `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
100
+
101
+ ## Hydration
102
+
103
+ On a document load every router hook renders what the server rendered until
104
+ the document has finished streaming, wherever the component sits. A component
105
+ in a `<Suspense>` or `loading()` boundary that streams in after the rest of
106
+ the page hydrates with the document's pathname, params, search params and
107
+ segments, an idle `useNavigation()` and `useAction()`, a `useLinkStatus()` that
108
+ is not pending, the handle data the HTML was rendered from, and `undefined`
109
+ location state, even when a navigation or an action has started meanwhile. It
110
+ cannot mismatch its server HTML.
111
+
112
+ - Navigation state, params and action state: the component shows the live
113
+ value right after it hydrates.
114
+ - Handle data a loader pushed late, and the entry's location state: every
115
+ reader gets them once the whole document has streamed and hydrated, in one
116
+ transition. A reader re-renders only when its selection changes.
117
+ - A navigation that commits before then mounts its page with the live values
118
+ from its first render. A boundary of the document that persists across that
119
+ navigation and has not hydrated yet then hydrates against the live values;
120
+ where they differ from its HTML, React reports the mismatch and renders that
121
+ boundary on the client.
@@ -39,6 +39,11 @@ the capture, so its pushes always apply client-side; with `ssr: false` they
39
39
  bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
40
40
  re-renders on every handle update, late loader pushes included.
41
41
 
42
+ On a document load `useHandle` hydrates with the handle data its HTML was
43
+ rendered from, wherever the reader sits; late loader pushes reach every reader
44
+ once the document has finished streaming and every boundary has hydrated (see
45
+ `SKILL.md`, "Hydration").
46
+
42
47
  Handles can be passed as props from server to client components:
43
48
 
44
49
  ```tsx
@@ -27,6 +27,34 @@ function IsLoading() {
27
27
  }
28
28
  ```
29
29
 
30
+ #### Pending UI, Back/Forward included
31
+
32
+ A link click, `router.push()` and a Back/Forward that has to fetch its entry
33
+ report a pending navigation the same way. While the page being left is still
34
+ on screen:
35
+
36
+ | What you want to show | Read |
37
+ | ---------------------------------------------- | ----------------------------------------------------------------------------------- |
38
+ | a request is out, nothing has arrived | `state === "loading"`; `pendingUrl` is the URL being fetched |
39
+ | the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
40
+ | which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
41
+
42
+ `state` and `pendingUrl` cover the network wait. Once the response has
43
+ arrived a loader may still be streaming, and React keeps the page being left
44
+ on screen where a navigation holds its content (a same-route change, a
45
+ `transition()` route); `isStreaming` covers that part too, so use it for a
46
+ progress indicator that should last until the new page is complete.
47
+
48
+ On Back/Forward the address bar changes at once, before any of this: during
49
+ the wait `window.location` is the entry being fetched, the same URL as
50
+ `pendingUrl`, and `location` is the page on screen. A Back/Forward served
51
+ from the client's history cache restores its page in one commit and reports
52
+ no pending state.
53
+
54
+ `location` moves when the response arrives. Where React then holds the page
55
+ being left, it is ahead of the screen for that last part of the wait, and so
56
+ is a URL hook in a component that first mounts then (issue #1046).
57
+
30
58
  ### useRouter()
31
59
 
32
60
  Access stable router actions. The returned object never changes identity, so
@@ -138,6 +166,16 @@ function Breadcrumbs() {
138
166
  const isShopRoute = useSegments((s) => s.path[0] === "shop");
139
167
  ```
140
168
 
169
+ The selector is applied when the router store changes (a navigation or new
170
+ handle data), as with `useNavigation` and `useAction`. A new selector function
171
+ on a re-render takes effect at the next change. To derive from a prop, select
172
+ the state and derive in render:
173
+
174
+ ```tsx
175
+ const segments = useSegments();
176
+ const part = segments.path[depth];
177
+ ```
178
+
141
179
  ### useLinkStatus()
142
180
 
143
181
  Track pending state inside a Link component. `pending` is `true` while a
@@ -84,6 +84,59 @@ definition:
84
84
  const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
85
85
  ```
86
86
 
87
+ ### When a reader sees an entry's state
88
+
89
+ A reader sees a history entry's location state together with that entry's
90
+ page, never before it. The router keeps the state of the entry on screen next
91
+ to the page it renders and changes both in one React commit. While a
92
+ navigation is pending (its loader still streaming, a `transition()` holding
93
+ the page) what is on screen keeps the state of the entry being left. That
94
+ holds for every reader in it, including one that mounts during the wait: it
95
+ reads the entry on screen, not the one `history.state` already holds.
96
+ Back/forward and state the server sets on a navigation
97
+ (`ctx.setLocationState()`, `redirect(url, { state })`) follow the same rule.
98
+
99
+ | What changes the state | A reader gets it |
100
+ | ------------------------------------------------------------ | ---------------------------------------------------------------- |
101
+ | `<Link state>`, `router.push()` / `router.replace()` | in the commit that shows the destination |
102
+ | back/forward | in the commit that restores the entry (cached or refetched) |
103
+ | the server, on a navigation (`setLocationState`, a redirect) | in the commit that shows the destination |
104
+ | a navigation the server answers with nothing to re-render | with the new location: the page on screen is the entry's |
105
+ | `router.push(url, { revalidate: false })` (no server fetch) | with the new location: the page does not change |
106
+ | a server action's `setLocationState` (no navigation) | when the action's response arrives, added to what readers show |
107
+ | `Def.write()` / `Def.delete()` | when the entry is next restored: back/forward to it, or a reload |
108
+
109
+ Three consequences:
110
+
111
+ - A value keeps its identity until the entry's state changes it. A navigation
112
+ that carries no state re-renders no reader. One that does re-renders each
113
+ reader once, in the commit that already renders the page; a reader of a
114
+ slot the navigation carried forward unchanged (the value it got from the
115
+ hook, passed back) or of a slot a server action did not touch gets the same
116
+ object again, so its effects and memoized children do not run. Any other
117
+ object is a new value, equal content included: a link clicked twice
118
+ announces its state twice. A value is always a copy of what was passed.
119
+ - `Def.write()` and `Def.delete()` change the history entry and nothing a
120
+ reader shows, whether it is mounted or mounts later. `Def.read()` returns
121
+ the written value at once.
122
+ - Outside the router's provider (a second React root, a component rendered
123
+ bare in a test) a reader has no entry and returns `undefined`. Test with
124
+ `renderRoute` (see `/testing`).
125
+
126
+ A reader that mounts with the destination reads the destination's state on
127
+ its first render. Inside an optimistically rendered `clientUrls()` destination
128
+ that is the state the navigation carries, before the server has answered (see
129
+ `/client-urls`, "Pitfalls").
130
+
131
+ The URL hooks follow the same rule for a component that is mounted across the
132
+ navigation: on back/forward too, `usePathname()` and `useSearchParams()`
133
+ change with the page, not at the `popstate` event (see
134
+ [`./urls.md`](./urls.md), "When the URL hooks change"). One difference
135
+ remains (issue #1046): a component that first mounts in the page being left,
136
+ after the response arrived and while React still holds the destination, reads
137
+ its own entry's location state from `useLocationState` and the destination's
138
+ URL and params from `usePathname()`, `useSearchParams()` and `useParams()`.
139
+
87
140
  ### State on router.push() / router.replace()
88
141
 
89
142
  The same `state` option exists on `router.push()` / `router.replace()` (see
@@ -198,12 +251,14 @@ function FlashBanner() {
198
251
  ```
199
252
 
200
253
  Flash behavior is determined by the definition (`{ flash: true }`), not by which
201
- hook reads it. `useLocationState` reads the value during render (on the
202
- hydration render it returns `undefined` and reads in a post-mount effect, so
203
- SSR and hydration agree), then clears it from `history.state` via
204
- `replaceState` in a `useEffect`. Multiple components reading the same flash
205
- definition all see the value. Pressing back/forward will not re-show the flash
206
- since it was cleared.
254
+ hook reads it. `useLocationState` reads the value during render (on a
255
+ document load it returns `undefined` until the document has finished streaming
256
+ and hydrating, so SSR and hydration agree), then clears it from `history.state` via
257
+ `replaceState` in a `useEffect`. Every component reading the same flash
258
+ definition sees the value, whenever it mounts, until the next navigation or
259
+ back/forward commits its entry's state: an entry that carries no flash shows
260
+ none. A server action's state does not end it. Pressing back/forward will not
261
+ re-show the flash since it was cleared.
207
262
 
208
263
  Set flash state from the server via `redirect()` with state (from a handler,
209
264
  middleware, or server action). Import the definition from the shared module so
@@ -221,7 +276,9 @@ import { FlashMessage } from "../location-states";
221
276
  };
222
277
  ```
223
278
 
224
- Or via `ctx.setLocationState()` on any response (handlers and middleware):
279
+ Or via `ctx.setLocationState()` from a handler or middleware. It is delivered
280
+ with a client navigation's or an action's response; a document response
281
+ carries no location state (there is no history entry to write it to yet):
225
282
 
226
283
  ```tsx
227
284
  (ctx) => {
@@ -230,6 +287,149 @@ Or via `ctx.setLocationState()` on any response (handlers and middleware):
230
287
  };
231
288
  ```
232
289
 
290
+ ### State from another deploy
291
+
292
+ History entries survive reloads and back/forward, so a tab left open across a
293
+ release still holds the state the older build wrote. The router drops it for
294
+ you: every entry records the app version its location state was written under,
295
+ and a read under any other version returns `undefined`, for typed slots and
296
+ for plain state alike. You do not manage a version and there is none to read;
297
+ the app version is the one the router already uses to reload a client that is
298
+ behind the server. It is computed from the app's built code (its server code
299
+ and the client assets), so it changes with a build that changes the app and
300
+ stays the same across a rebuild or redeploy of unchanged code.
301
+
302
+ | An entry's location state, read by | Result |
303
+ | ---------------------------------------------------------------- | ----------- |
304
+ | the version that wrote it: navigation, back/forward, refresh | the value |
305
+ | the same code rebuilt or redeployed | the value |
306
+ | a changed build, after a refresh or a restored tab | `undefined` |
307
+ | a changed build, on back/forward to an entry the older one wrote | `undefined` |
308
+ | any build, when the entry predates this behavior | `undefined` |
309
+
310
+ So location state is for the current session of the current version of the
311
+ app: a filter carried on a link, a flash message, a scroll target. State that
312
+ has to outlive a deploy that changes the app belongs in the URL, a cookie or
313
+ storage. In development a
314
+ server-module edit (HMR) changes the version too; the running page keeps its
315
+ state, and the next document load drops it.
316
+
317
+ ### createLocationState options
318
+
319
+ ```ts
320
+ createLocationState<TState>({
321
+ flash?: boolean, // read once, cleared after paint (above)
322
+ clearOnReload?: boolean, // drop the state on a document load (below)
323
+ });
324
+ ```
325
+
326
+ The value is always stored as-is. `clearOnReload` changes the slot's KEY in
327
+ `history.state` instead, by a suffix on the key the Vite plugin injects
328
+ (`<key>` below, `__rsc_ls_<file>#<ExportName>`):
329
+
330
+ | Options | Key in `history.state` | Reads `undefined` when |
331
+ | --------------- | ---------------------- | ----------------------------------------------- |
332
+ | none, `flash` | `<key>` | the slot is empty (`flash`: once read) |
333
+ | `clearOnReload` | `<key>~r` | the entry's document was loaded since the write |
334
+
335
+ `Def.__rsc_ls_key` is that key, suffix included. `~` cannot appear in an export
336
+ name, so no other definition can end up with a suffixed key.
337
+
338
+ `flash` with `clearOnReload` is rejected: `createLocationState` throws in
339
+ development.
340
+
341
+ ### clearOnReload: state the server did not render with
342
+
343
+ Location state lives in the browser, so the server renders a document without
344
+ it. `useLocationState` therefore hydrates as `undefined` and applies the stored
345
+ value once the document has finished streaming and every boundary has
346
+ hydrated. For most state that is invisible. For state that decides
347
+ how much content is on the page, it is a layout shift on every refresh.
348
+
349
+ The case this option exists for is a "load more" list. `?page=6` loads that
350
+ page's 50 products through a loader, and the 250 already on screen ride along
351
+ as location state on the link, so the next page shows them at once and
352
+ streams the new ones:
353
+
354
+ ```ts
355
+ // location-states.ts
356
+ import { createLocationState } from "@rangojs/router";
357
+
358
+ export const CarriedProducts = createLocationState<Product[]>({
359
+ clearOnReload: true,
360
+ });
361
+ ```
362
+
363
+ ```tsx
364
+ "use client";
365
+ import { Link, useLoader, useLocationState } from "@rangojs/router/client";
366
+ import { ProductsLoader } from "./loaders"; // the page named by ?page
367
+ import { CarriedProducts } from "./location-states";
368
+
369
+ export function ProductList() {
370
+ const { data } = useLoader(ProductsLoader);
371
+ const carried = useLocationState(CarriedProducts) ?? [];
372
+ const products = [...carried, ...data.products];
373
+
374
+ return (
375
+ <>
376
+ <ul>
377
+ {products.map((product) => (
378
+ <li key={product.id}>{product.name}</li>
379
+ ))}
380
+ </ul>
381
+ <Link
382
+ to={`/products?page=${data.page + 1}`}
383
+ state={[CarriedProducts(products)]}
384
+ scroll={false}
385
+ >
386
+ Load more
387
+ </Link>
388
+ </>
389
+ );
390
+ }
391
+ ```
392
+
393
+ The list is a plain concatenation. After "Load more" the next page's loader
394
+ streams while the current page stays on screen, and `carried` stays the
395
+ current entry's until that page lands: the carried products and the loader's
396
+ page change in one commit, so no product is ever listed twice.
397
+
398
+ Without the option, a refresh of `?page=6` renders 50 products on the server
399
+ and then inserts the stored 250 above them. With it, the refreshed page stays
400
+ as the server rendered it:
401
+
402
+ | What happens to the entry | A `clearOnReload` slot |
403
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------- |
404
+ | The reader mounts during a client navigation (`<Link>`, `router.push`) | applied, as without the option |
405
+ | back/forward inside the running app (popstate), a server action's state | applied, as without the option |
406
+ | A document load: refresh, or a back/forward that loads the document | not applied, and removed from `history.state` |
407
+ | Later on that page: a reader mounts, or a navigation returns to the entry | still `undefined`; the next write (the next "Load more") stores anew |
408
+ | The page is restored from the browser's back/forward cache | untouched: nothing is loaded or hydrated |
409
+
410
+ - The rule is any document load of the entry, not only the Reload button:
411
+ restoring a closed tab or duplicating a tab loads the document too.
412
+ - Only the entry being loaded is cleared. Going back from it to an earlier
413
+ entry inside the running app is a client navigation and applies that
414
+ entry's state.
415
+ - The router removes every `~r` key when the client starts, before hydration.
416
+ It goes by the key alone, so it does not matter whether a reader is mounted,
417
+ where it sits (a `<Suspense>` boundary that hydrates late included), or
418
+ whether the definition's module is loaded at all. Other slots on the entry
419
+ are left alone.
420
+ - State the server sets is never dropped by this: a document response carries
421
+ no location state, and `ctx.setLocationState()` / `redirect(url, { state })`
422
+ reach `history.state` only through navigations and actions in the running
423
+ app, after start-up.
424
+ - `.write()` stores under the same key, so it does not survive a refresh
425
+ either.
426
+ - Adding the option to an existing definition moves it to the `~r` key: state
427
+ stored before, under the plain key, is no longer read (and is not removed).
428
+ Removing the option moves it back to the plain key; what was stored under
429
+ `~r` is removed at the next document load.
430
+ - It cannot be combined with `flash`: flash state is removed at its first
431
+ read, so the pair could only drop a message nobody has seen yet.
432
+
233
433
  ### .read() (non-hook access)
234
434
 
235
435
  Read current location state outside React components (client-side only):
@@ -264,9 +464,11 @@ Static counterparts to `.read()`. Both mutate the current history entry's
264
464
  bookkeeping, other location state slots). Both are client-only; they throw
265
465
  when called on the server.
266
466
 
267
- Neither dispatches an event, so components reading via `useLocationState`
268
- will NOT re-render until the next navigation/popstate. Pair with `.read()`
269
- (or a fresh mount via back/forward/reload) instead.
467
+ Neither changes what a `useLocationState` reader shows, mounted or mounting
468
+ later: readers get the entry's state as it was committed (see "When a reader
469
+ sees an entry's state"). The written value is there for `.read()` at once,
470
+ and for readers when the entry is next restored (back/forward to it, or a
471
+ reload).
270
472
 
271
473
  ```tsx
272
474
  "use client";
@@ -275,7 +477,7 @@ import { ProductState } from "./state";
275
477
  // Persisted across hard refresh and back/forward of this entry.
276
478
  ProductState.write({ name: "Widget", price: 9.99 });
277
479
 
278
- // Read later (or on next mount).
480
+ // Read later.
279
481
  const current = ProductState.read();
280
482
 
281
483
  // Manually clear the slot. Idempotent if it isn't set.
@@ -47,7 +47,7 @@ function CurrentPage() {
47
47
  }
48
48
  ```
49
49
 
50
- Returns the pathname string without search params or hash. Updates on navigation commit.
50
+ Returns the pathname string without search params or hash. It is the pathname of the page on screen: it changes when a navigation's destination commits, Back/Forward included (see [When the URL hooks change](#when-the-url-hooks-change)).
51
51
 
52
52
  ### useSearchParams()
53
53
 
@@ -109,6 +109,45 @@ Options: `replace` (default false — push), `scroll` (default true),
109
109
  `revalidate` (default true; `false` skips the server fetch — legal because
110
110
  the setter never changes the pathname).
111
111
 
112
+ ### When the URL hooks change
113
+
114
+ `usePathname()`, `useSearchParams()` and `useParams()` describe the page on
115
+ screen. They change in the commit that shows a navigation's destination, not
116
+ when the navigation starts. That holds for Back/Forward as it does for a link
117
+ click or `router.push()`.
118
+
119
+ | Navigation | The hooks change |
120
+ | ------------------------------------------------- | -------------------------------------------------------------------------- |
121
+ | `<Link>`, `router.push()` / `router.replace()` | when the destination commits |
122
+ | Back/Forward, entry in the client's history cache | with the restored page, in one commit |
123
+ | Back/Forward, entry fetched again | when the fetched page commits; until then the page being left is on screen |
124
+ | a Back/Forward whose fetch fails | with the error boundary: they report the entry the browser is on |
125
+ | `router.push(url, { revalidate: false })` | at once: nothing is fetched and the page stays |
126
+ | inside an optimistic `clientUrls()` destination | at the click, for that branch only (see `/client-urls`) |
127
+
128
+ **The hooks can disagree with the address bar.** On Back/Forward the browser
129
+ changes the URL and `history.state` before it tells the router. When the
130
+ entry has to be fetched (it left the client's history cache, or the cache was
131
+ cleared), the page being left stays on screen for the wait and the hooks keep
132
+ reporting its URL, while `window.location` is already the destination's. That
133
+ is deliberate: an active-link highlight, a breadcrumb or a filter panel
134
+ derived from the hooks stays in step with the content next to it. For
135
+ anything you render, read the hooks, not `window.location`.
136
+
137
+ For pending UI during that wait read `useNavigation()` (see
138
+ [`./navigation.md`](./navigation.md)): `state === "loading"` and `pendingUrl`
139
+ (the entry being fetched) while the request is out, `isStreaming` for the
140
+ whole wait. A Back/Forward served from the history cache has no wait.
141
+
142
+ `setSearchParams((prev) => ...)` builds on the same location: called during a
143
+ pending Back/Forward, `prev` is the search of the page on screen.
144
+
145
+ One gap (issue #1046): a component that first mounts in the page being left
146
+ after the navigation's response has arrived, while React still holds the
147
+ destination behind a loader or a `transition()`, reads the destination's URL
148
+ and params from these hooks. Components that were already mounted are not
149
+ affected. It applies to a push and to Back/Forward alike.
150
+
112
151
  ### useHref()
113
152
 
114
153
  Mount-aware href for client components inside `include()` scopes:
@@ -370,3 +370,34 @@ apps; each app owns its own document.
370
370
 
371
371
  **Within-app** navigation is unchanged — a normal soft SPA update (the document
372
372
  stays mounted). Only crossing an app boundary triggers the reload.
373
+
374
+ ## Deploys and the cache
375
+
376
+ Each `createRouter()` app has its own cache version, computed by `vite build` from that app's built code, so deploying a change to one app does not clear the cache of the others. Lazily mounting sub-apps (`.lazy(() => import("./apps/admin"))`) is what keeps them independent: an app's version covers everything the modules that statically import its router can run, and a lazy mount is where that stops. Routers that one module imports statically share that module's code, so a change to either moves both versions.
377
+
378
+ | Deploy | Cached data | Stored HTML | Open tabs |
379
+ | ------------------------------------- | ------------------- | --------------------- | ----------------------- |
380
+ | Rebuild, no code change | kept | kept | untouched |
381
+ | Server code of app A changes | cleared for A only | cleared for A only | A's tabs reload |
382
+ | Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
383
+ | Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
384
+
385
+ The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, change that app's `version` or invalidate its tags. See `/cache-guide`.
386
+
387
+ ## Shared cache store
388
+
389
+ Sub-apps may share one cache store (one KV namespace, one `MemorySegmentCacheStore`). Every entry that is a router's output for a host and path is keyed by that router as well; its key starts with the router's id (`{routerId}@{host}{path}...`):
390
+
391
+ - PPR shells (document, navigation and build-time);
392
+ - route `cache()` records, document-cache responses and response-route entries;
393
+ - a loader's own `cache()` entry, and a `"use cache"` entry of a function called with a handler or loader `ctx`. A `ctx` is one router's: its context variables, its `env`, its `reverse()` map.
394
+
395
+ So two sub-apps never read each other's entry, including where they serve the same host and path:
396
+
397
+ - with the `hostOverride` cookie, which picks the app and forwards the request unchanged, so every app answers under the one preview origin;
398
+ - after a `fallback()` that hands a request to another app's handler;
399
+ - after a `router.prerender()` warm that resolved another app's host as its origin.
400
+
401
+ There is nothing to configure and no namespace to manage. What it relies on is that each router has a stable id: the one the Vite plugin injects for a `createRouter({ ... })` call written with an object literal, or the `id` you pass (`createRouter({ id: "admin", ... })`). A router the plugin cannot reach, for example `createRouter(options)` with an options variable, falls back to a counter (`router_0`, `router_1`, ...) that follows module evaluation order. One such router is fine. With more than one, the build already warns (`N routers use auto-generated IDs`), because routes and cache versions are matched by the same id; the fix is the same here: pass the object literal inline or set an `id`.
402
+
403
+ Not keyed by router, on purpose: a `"use cache"` entry of a function called with plain arguments or the bare `Request` (the function and its arguments name no router, so both apps share the entry), tag markers (an invalidation reaches every app), and any key you build yourself (`cache({ key })` on a route or a loader, the document cache's `keyGenerator`): keep those apart yourself where two apps must not share them.
@@ -238,6 +238,12 @@ always activates. `when` is not re-evaluated during action revalidation, so
238
238
  an open modal stays open after an action. A selector that throws does not
239
239
  intercept: the full page renders and the error is logged with the route name.
240
240
 
241
+ The server learns `from` from the `X-RSC-Router-Client-Path` header, which the
242
+ rango client sends on every navigation. A partial request without it falls
243
+ back to `Referer`, and only when that `Referer` is on the request's own origin
244
+ (scheme, host and port). A request with no usable navigation context is not
245
+ intercepted: it gets the full page.
246
+
241
247
  A prefetch of a route an intercept targets is stored per source page, whether
242
248
  or not the intercept applies from that page (`rsc-rendering.ts`
243
249
  `x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A