@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loader
3
- description: Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, or must be settled in the SSR'd document (ssr:false).
3
+ description: Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, must be settled in the SSR'd document (ssr:false), or must stay out of link prefetches (prefetch:false).
4
4
  argument-hint: "[loader]"
5
5
  ---
6
6
 
@@ -285,7 +285,9 @@ See `/route` for the full handler-context contract (shared with loaders) and
285
285
 
286
286
  - `ctx.params` — merged route params + explicit loader params. For fetchable
287
287
  loaders called with `load(Loader, { params: { ... } })`, explicit params
288
- override route-matched params.
288
+ override route-matched params. Explicit params are string values; a request
289
+ with another value or a `__proto__`, `constructor` or `prototype` key is
290
+ rejected (400, `load()` fails with `Invalid loader params`).
289
291
  - `ctx.routeParams` — server-trusted route params from URL pattern matching.
290
292
  Cannot be overridden by client-provided params.
291
293
 
@@ -333,6 +335,10 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
333
335
  ]);
334
336
  ```
335
337
 
338
+ In a `clientUrls()` group the same `loader(Def, () => [revalidate(fn)])` form
339
+ runs on the server with these arguments; the build moves the predicate out of
340
+ the `"use client"` module (`/client-urls` → "Revalidation runs on the server").
341
+
336
342
  ### `revalidate()` return shapes
337
343
 
338
344
  > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
@@ -936,6 +942,8 @@ function ProductPage() {
936
942
  }
937
943
  ```
938
944
 
945
+ A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
946
+
939
947
  ## Loader Authority: notFound() and redirect()
940
948
 
941
949
  A loader may **throw** `notFound()` and `redirect()` — data-dependent
@@ -1094,6 +1102,195 @@ The costs and constraints:
1094
1102
  Also available in `clientUrls()` route groups (`/client-urls`), where the
1095
1103
  loader-heavy shape makes it most useful.
1096
1104
 
1105
+ ## `prefetch: false` — Keep a Loader Out of Prefetches
1106
+
1107
+ A `<Link>` prefetch is a navigation sent early: the server renders every
1108
+ handler and every loader for it. With the default `"viewport"` strategy that
1109
+ means a grid of forty product links runs forty copies of your slowest loader
1110
+ for pages most visitors never open. Pass `prefetch: false` to keep one loader
1111
+ out:
1112
+
1113
+ ```tsx
1114
+ import { Suspense } from "react";
1115
+
1116
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
1117
+ loader(PriceLoader), // cheap: still runs in the prefetch
1118
+ loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in a prefetch
1119
+ loading(<ProductSkeleton />),
1120
+ ]),
1121
+ ```
1122
+
1123
+ (`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
1124
+ `{ ssr: false, prefetch: false }` if you need both.)
1125
+
1126
+ What happens:
1127
+
1128
+ - A **prefetch** of the route does not run the loader and does not read its
1129
+ `cache()`; the payload marks it deferred. The rest of the page prefetches as
1130
+ usual.
1131
+ - Only on a route the browser does not show yet. A loader the page already
1132
+ holds, on a same-route navigation such as `/product/a` to `/product/b`, runs
1133
+ in the prefetch when it revalidates, exactly as without the flag, and the
1134
+ click sends no second request. The rule: a click that adopts a prefetch is
1135
+ never worse than the same click with no prefetch at all (one exception: a
1136
+ click before its prefetch has answered, under "The limits"), and content on
1137
+ screen is never swapped for a fallback because of the flag. For the same
1138
+ reason a prefetch that deferred something is used only on the page that
1139
+ made it: a page that already shows the segment makes its own prefetch.
1140
+ - A **click** that adopts that prefetch is never blocked by the loader. A
1141
+ deferred loader behaves like a loader that is still streaming, and where
1142
+ the page reads it (`useLoader(ReviewsLoader)`) React does what it does for
1143
+ one:
1144
+ - the read has its own `<Suspense>`: the page shows at once with that
1145
+ fallback in it. This can be a finer fallback than the same click shows
1146
+ with no prefetch, where the whole route is still streaming behind its
1147
+ `loading()`;
1148
+ - the nearest boundary is the route's `loading()`: that fallback shows;
1149
+ - no boundary at all: React holds the page being left until the value
1150
+ arrives.
1151
+
1152
+ Meanwhile the browser sends one second request, the fill, that runs only
1153
+ the deferred loader. Its value arrives in the page that is already on
1154
+ screen, as a streamed loader's does; nothing remounts.
1155
+
1156
+ - A **document request** (hard load), a **navigation with no prefetch to
1157
+ adopt**, an action, back/forward and a no-JS form post run the loader as
1158
+ they always did. Leaving the option out is the old behavior.
1159
+
1160
+ Read the loader behind a boundary, as you would any streamed loader:
1161
+
1162
+ ```tsx
1163
+ "use client";
1164
+ function Reviews() {
1165
+ const { data } = useLoader(ReviewsLoader);
1166
+ return <ReviewList items={data} />;
1167
+ }
1168
+
1169
+ // in the server component:
1170
+ <Suspense fallback={<ReviewsSkeleton />}>
1171
+ <Reviews />
1172
+ </Suspense>;
1173
+ ```
1174
+
1175
+ The limits:
1176
+
1177
+ - **A click before its prefetch has answered waits for it, then fetches the
1178
+ deferred part.** The flag is there to keep expensive or slow work out of
1179
+ every prefetch, so that an API that is costly to call is not called for
1180
+ every link a visitor might click. A click that comes while the link's
1181
+ prefetch is still in flight reuses that prefetch, as it does without the
1182
+ flag, and sends the fill when the prefetch answers. The page shows no later
1183
+ than with no prefetch; the deferred part shows its fallback until the fill
1184
+ lands, and can complete later than a click with no prefetch, by up to what
1185
+ was left of the prefetch. That is the price of keeping the work out of the
1186
+ prefetch.
1187
+ - **A navigation started before the fill answers cancels it.** It is
1188
+ cancelled when the newer navigation starts, not when that one commits. If
1189
+ the newer navigation then ends without replacing the page (a redirect the
1190
+ client refuses, an action refetch that drops its response), the deferred
1191
+ part keeps its fallback until the next navigation. Every other ending
1192
+ replaces the page anyway.
1193
+ - **`navigate()` resolves after the fill has answered.** The click's
1194
+ navigation fills the deferred part itself, so `await navigate(url)` (and a
1195
+ `router.push()` you await) settles a round trip after the page committed.
1196
+ `useNavigation()` reads `idle` from the commit, with `isStreaming` true
1197
+ until the fill has streamed in.
1198
+ - **A boundary is required for a fallback.** With no `loading()` on the entry
1199
+ and no `<Suspense>` around the read, nothing can show a fallback, so React
1200
+ holds the page being left on screen until the fill returns.
1201
+ `useNavigation()` reads `state: "loading"` for that time, as on any
1202
+ navigation that is still streaming, so a progress bar shows, and the URL
1203
+ hooks (`useNavigation().location`) change with the page. The address bar
1204
+ has already changed: it moves with the click, where a click with no
1205
+ prefetch moves it when its response starts. There is no definition-time
1206
+ error.
1207
+ - **`ctx.rendered()` is refused.** A loader that can be deferred throws on
1208
+ every request kind (a document load included, so you see it the first time
1209
+ you open the page in development) if it calls `await ctx.rendered()`. The
1210
+ fill does not re-run the entry's handler, so there is no render to wait for
1211
+ and no handle data to read. Remove `{ prefetch: false }` from the loader, or
1212
+ stop calling `ctx.rendered()`. Pushing a handle from the loader is fine.
1213
+ - **`ctx.set()` values from held handlers are not visible.** The fill runs the
1214
+ deferred work and middleware only. A handler or layout the client already
1215
+ holds does not run again, so a variable it set with `ctx.set()` is missing
1216
+ when the loader reads it with `ctx.get()`. Variables set by middleware are
1217
+ there, as middleware runs on every request; so are `ctx.params` and
1218
+ `ctx.request`.
1219
+ - **`redirect()`, `notFound()` and handle pushes arrive late.** They behave as
1220
+ for a loader that streams behind `loading()`, but they come with the fill,
1221
+ after the page is already on screen: a redirect becomes a replace
1222
+ navigation, `notFound()` renders the nearest not-found boundary, an error
1223
+ reaches the error boundary.
1224
+ - **The fill reads through the loader's own caching; a prefetch reads none
1225
+ of it, by intent.** A loader with `cache()` that nothing in the prefetch
1226
+ reads is skipped even when its entry is warm, and so is a `"use cache"`
1227
+ function it calls: a prefetch that looked would have to run the loader on
1228
+ a miss, which is the work the flag keeps out. The fill is where the loader
1229
+ runs. It serves a warm `cache()` entry without running the loader, runs
1230
+ the loader and stores on a cold one, and a `"use cache"` function inside
1231
+ the loader hits or stores as on any request.
1232
+ - **A loader something else reads with `ctx.use()` runs in the prefetch.**
1233
+ The reader needs the value, so the flag cannot keep the loader out. When
1234
+ the read is in time, the loader is an ordinary loader for that prefetch,
1235
+ its own `cache()` included: one run (none on a warm entry), the reader and
1236
+ the loader's data agree, nothing deferred, no fill for it. In time means:
1237
+ inside a handler that has no `loading()` (the router waits for it), or
1238
+ before the reader's first `await`.
1239
+ - **Two late readers make it run twice.** The loader runs in the prefetch for
1240
+ its reader and again in the fill, because by the time the read happens the
1241
+ loader's own segment has already been sent as deferred:
1242
+
1243
+ ```tsx
1244
+ // 1. A handler under loading() is streamed: the router does not wait for
1245
+ // it, so a read after its first await comes late.
1246
+ path(
1247
+ "/product/:id",
1248
+ async (ctx) => {
1249
+ const product = await getProduct(ctx.params.id);
1250
+ const reviews = await ctx.use(ReviewsLoader); // late: runs it here
1251
+ return <Product product={product} reviews={reviews} />;
1252
+ },
1253
+ { name: "product" },
1254
+ () => [
1255
+ loader(ReviewsLoader, { prefetch: false }), // and the fill runs it again
1256
+ loading(<ProductSkeleton />),
1257
+ ],
1258
+ );
1259
+
1260
+ // 2. A loader that is not deferred itself and reads it after an await.
1261
+ const SummaryLoader = createLoader(async (ctx) => {
1262
+ const product = await getProduct(ctx.params.id);
1263
+ const reviews = await ctx.use(ReviewsLoader); // late: two runs
1264
+ return summarize(product, reviews);
1265
+ });
1266
+ ```
1267
+
1268
+ Data is never missing, only fetched twice, and the page shows the fill's
1269
+ value. To get one run, start the read before the first `await`
1270
+ (`const reviews = ctx.use(ReviewsLoader)` at the top, awaited where you
1271
+ need it), or drop the flag from a loader the page cannot render without.
1272
+
1273
+ - **`clientUrls()` routes take it on `loader()`.** In a `clientUrls()`
1274
+ definition `loader(Def, { prefetch: false })` works as here: a click
1275
+ inside the group shows the destination at once, commits the prefetched
1276
+ data with the URL, and one fill runs the flagged loaders (see
1277
+ `/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
1278
+ on a group route, from the group or from a server layout above it.
1279
+ - **No effect inside `intercept()`.** Its loaders accept the flag and ignore
1280
+ it, in `urls()` and in `clientUrls()`: a prefetch that resolves an
1281
+ intercept defers nothing.
1282
+ - **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
1283
+ bake lane and is served from the shell, so `prefetch: false` is ignored for
1284
+ it and development logs a warning. Under `cache()` and `Prerender` the flag
1285
+ works as above (see `/caching`, `/prerender`, `/ppr`).
1286
+
1287
+ To skip a handler along with its loaders, flag the `loading()` instead (see
1288
+ `/route` → "`loading()` options"). `loader(Def, { prefetch: false })` accepts
1289
+ only a boolean; anything else throws `loader() prefetch must be a boolean`.
1290
+
1291
+ Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
1292
+ and `result.readDeferred()` (see `/testing`).
1293
+
1097
1294
  ## Fetchable Loaders
1098
1295
 
1099
1296
  By default, loaders only run as part of a render (document, navigation, or
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: middleware
3
- description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.
3
+ description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects, rewrites (throw rewrite() renders another route at the same URL). Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, showing a maintenance page at the visitor's URL, or deciding whether middleware wraps server actions.
4
4
  argument-hint: [middleware-name]
5
5
  ---
6
6
 
@@ -19,7 +19,7 @@ There are two levels of middleware with different execution scopes:
19
19
 
20
20
  ### Global middleware (`router.use()`)
21
21
 
22
- Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
22
+ Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders. A request a middleware rewrites (`throw rewrite()`, below) runs the chain again for the target: once per pass.
23
23
 
24
24
  ```typescript
25
25
  const router = createRouter<AppEnv>({})
@@ -257,6 +257,8 @@ export const myMiddleware: Middleware = async (ctx, next) => {
257
257
  ctx.headers; // response Headers (stub before next(), the real response after)
258
258
  ctx.setLocationState(FlashMessage({ text: "Saved" })); // history state for the client
259
259
  ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill)
260
+ ctx.rewrittenFrom; // the visitor's URL when this request was rewritten, else undefined
261
+ ctx.isRewrite(error); // true for a thrown rewrite() (rethrow it from a try/catch)
260
262
 
261
263
  // Opt the current request out of PPR shell lookup/capture.
262
264
  ctx.dynamic();
@@ -428,6 +430,185 @@ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{
428
430
 
429
431
  A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
430
432
 
433
+ ## Rewrite: another route at the same URL
434
+
435
+ `throw rewrite(target)` renders another route while the address bar keeps the
436
+ visitor's URL: a maintenance or lock page, a deploy migration, a URL override,
437
+ an A/B or geo split. A redirect changes the URL; a rewrite does not.
438
+
439
+ ```typescript
440
+ import { getRequestContext, rewrite } from "@rangojs/router";
441
+ import type { Middleware } from "@rangojs/router";
442
+
443
+ export const maintenance: Middleware = async (ctx, next) => {
444
+ if (!locked || ctx.pathname === "/maintenance") return next();
445
+ // Mutations get a Response: a rewrite cannot answer a server action or a form post.
446
+ if (ctx.request.method !== "GET") {
447
+ return new Response("Maintenance", {
448
+ status: 503,
449
+ headers: { "Retry-After": "600" },
450
+ });
451
+ }
452
+ ctx.set(Lock, { until });
453
+ throw rewrite("maintenance");
454
+ };
455
+
456
+ // urls.tsx — the target sets its own status
457
+ path("/maintenance", MaintenancePage, { name: "maintenance" }, () => [
458
+ middleware(async (ctx, next) => {
459
+ getRequestContext().setStatus(503);
460
+ ctx.header("Retry-After", "600");
461
+ await next();
462
+ }),
463
+ ]);
464
+ ```
465
+
466
+ Targets, like `ctx.reverse` with a path form:
467
+
468
+ ```typescript
469
+ throw rewrite("/maintenance"); // a path (basename-prefixed like redirect())
470
+ throw rewrite("/maintenance?from=shop"); // a path with search
471
+ throw rewrite("maintenance"); // a global route name without params
472
+ throw rewrite("product", { slug }); // with params
473
+ throw rewrite("product", { slug }, { tab: "reviews" }); // and search
474
+ ```
475
+
476
+ **How it runs.** The throw unwinds the request to the router, which runs it
477
+ again as the target, from the top, as a direct visit would: global middleware
478
+ matched on the target's path (a `.use("/admin/*", guard)` or an inline
479
+ `ctx.pathname` check decides on the rendered route), the target's route
480
+ middleware, its render. Code after `await next()` in the pass that rewrote
481
+ never runs, and global middleware runs once per pass.
482
+
483
+ **Never skip a guard on a rewritten pass.** Authorization, auth and
484
+ rate-limit checks run on every pass, as written: the target's guards
485
+ deciding on the rendered route is what keeps a rewrite from becoming a way
486
+ around them. `if (!ctx.rewrittenFrom)` is only for side effects you want
487
+ once per request, such as a counter or a log line:
488
+
489
+ ```typescript
490
+ export const countVisit: Middleware = async (ctx, next) => {
491
+ if (!ctx.rewrittenFrom) visits.increment(); // once per request
492
+ return next();
493
+ };
494
+ ```
495
+
496
+ **Throw it before `await next()`.** Once your `next()` ran the route, the
497
+ page has rendered: a `throw rewrite()` after it (a "rewrite on 404" after
498
+ `const res = await next()`) is an error, "throw rewrite() before await
499
+ next()". Decide before calling `next()`. A rewrite thrown further down
500
+ passes through your `await next()` as usual, and in a `catch` around it you
501
+ may throw another rewrite in its place; after any other failure of `next()`
502
+ (the route may have rendered before it failed) a rewrite is an error too.
503
+
504
+ **Same request context.** Values you `ctx.set()` before the throw are
505
+ readable in the target's middleware, handlers and loaders; cookies, headers
506
+ and the status you set stay on the response; location state, theme and
507
+ `ctx.dynamic()` carry over. The request, URLs, params and route name are the
508
+ target's.
509
+
510
+ **Old and new URL.** `ctx.url`, `ctx.pathname` and `ctx.searchParams` are
511
+ the target's; `ctx.rewrittenFrom` is the visitor's URL (search included; the
512
+ first URL after several rewrites; `undefined` when nothing rewrote).
513
+ `usePathname()` and `useSearchParams()` show the visitor's URL in the server
514
+ HTML and after hydration, except in a render other visitors are served (see
515
+ Caches); `useParams()` and the route name are the target's. The response
516
+ carries `x-rango-rewrite: <target pathname>`.
517
+
518
+ **`ctx.rewrittenFrom` is request identity**, like `cookies()`: reading it
519
+ inside a `cache()` boundary or a `"use cache"` function throws (pass what
520
+ you need in from middleware), and a render other visitors may be served (a
521
+ PPR shell capture, a page the document cache stores) reads `undefined`, as a
522
+ direct visit does.
523
+
524
+ **Middleware only, by design.** `rewrite()` is a middleware tool. A route
525
+ handler or a loader (any loader, `ssr: false` included) cannot rewrite: its
526
+ throw is an error whose message points you to route middleware. A rewrite
527
+ that depends on data goes in the route's middleware, reading the data
528
+ through a cached function the loader shares, so the lookup runs once:
529
+
530
+ ```typescript
531
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
532
+ middleware(async (ctx, next) => {
533
+ const product = await getProduct(ctx.params.slug); // cached, shared with the loader
534
+ if (product.discontinued) {
535
+ throw rewrite("discontinued", { slug: ctx.params.slug });
536
+ }
537
+ await next();
538
+ }),
539
+ loader(productLoader), // calls getProduct(slug): a cache hit
540
+ ]);
541
+ ```
542
+
543
+ **Throw it, don't return it.** A returned rewrite is an error. Where a
544
+ rewrite cannot be honored it is an error that names the rule: a route
545
+ handler or a loader (see above), a server action (use `redirect()`),
546
+ intercept middleware, a loader fetch (`useFetchLoader`, `load()`: it
547
+ rejects with the rule) or its middleware, and middleware handling a non-GET
548
+ request (a JS action or a no-JS form post: return a `Response`).
549
+
550
+ **try/catch around next().** A thrown rewrite passes through your
551
+ middleware's `catch`. Rethrow it:
552
+
553
+ ```typescript
554
+ export const errorPage: Middleware = async (ctx, next) => {
555
+ try {
556
+ return await next();
557
+ } catch (error) {
558
+ if (ctx.isRewrite(error)) throw error;
559
+ return new Response("Something went wrong", { status: 500 });
560
+ }
561
+ };
562
+ ```
563
+
564
+ **Loops.** More than 5 rewrites in one request, or a rewrite to the URL the
565
+ request is on, answers 508 and reports a `RewriteLoopError` (its message has
566
+ the trail, `/a -> /b -> /a`) through `onError` with phase `"routing"`.
567
+
568
+ **Caches.**
569
+
570
+ - The document cache keys a rewritten request by the target's URL, as a
571
+ direct visit. Register the rewriting middleware **before**
572
+ `createDocumentCacheMiddleware()`: a cache hit for the visitor's URL would
573
+ be served before your middleware runs. A page the document cache stores,
574
+ and a PPR shell of the target, are rendered as a direct visit to the
575
+ target: they carry the target's pathname and `ctx.rewrittenFrom` is
576
+ `undefined` there, so a component that renders `usePathname()` can
577
+ mismatch on hydration at the visitor's URL when such a page is served to
578
+ a rewritten request.
579
+ - The client's prefetch and history caches keep a rewritten page under the
580
+ visitor's URL, and nothing tells them about the rewrite: a page prefetched
581
+ during a lock shows the lock page for up to `prefetchCacheTTL` after the
582
+ lock lifts. `invalidateClientCache()` drops a client's caches, for the
583
+ client that receives the response; mark the clients that saw the lock and
584
+ call it on their first response after it lifts:
585
+
586
+ ```typescript
587
+ import { cookies, invalidateClientCache, rewrite } from "@rangojs/router";
588
+
589
+ export const maintenance: Middleware = async (ctx, next) => {
590
+ if (!locked) {
591
+ if (cookies().get("saw-lock")) {
592
+ cookies().delete("saw-lock");
593
+ invalidateClientCache();
594
+ }
595
+ return next();
596
+ }
597
+ cookies().set("saw-lock", "1", { path: "/" });
598
+ // ...the lock as above
599
+ throw rewrite("maintenance");
600
+ };
601
+ ```
602
+
603
+ **Testing.** `runMiddleware` reports a thrown rewrite as `result.rewrite`
604
+ (`{ pathname, search, routeName? }`). `serveShellRequest` takes a rewritten
605
+ page request through the full production pipeline (document or `partial`
606
+ navigation): the target's middleware, render, status and `x-rango-rewrite`.
607
+ `dispatch` re-dispatches too, for response routes only: it matches the
608
+ visitor's URL first and throws on a component (RSC) route.
609
+ `renderRoute(..., { request: "/shop", rewrite: "/maintenance" })` renders
610
+ the target's route at the visitor's URL. See `/testing`.
611
+
431
612
  ## Authentication Middleware
432
613
 
433
614
  ```typescript
@@ -850,8 +850,8 @@ For **dashboard / admin / settings-shaped sections** — high navigation
850
850
  frequency inside one layout, mostly tab/param/filter switches — also consider
851
851
  porting that route group to `clientUrls()` (`/client-urls`): the definition
852
852
  matches in the browser (instant optimistic pending, no server round-trip to
853
- start a transition) and browser-run `revalidate()` predicates hold data across
854
- switches that don't invalidate it, which is the fastest transition shape Rango
853
+ start a transition) and per-loader `revalidate()` predicates (run on the
854
+ server) hold data across switches that don't invalidate it, which is the fastest transition shape Rango
855
855
  has. Server-component routes and `clientUrls()` groups compose in one tree.
856
856
 
857
857
  ## 7. Server Actions
@@ -84,21 +84,24 @@ per app (both compose in one `urls()` tree via `include()`):
84
84
  | component (client, hooks) | stays a client component — no conversion |
85
85
  | `loader` (throws `redirect`/404) | `createLoader()` — thrown `redirect()`/`notFound()` kept |
86
86
  | `useLoaderData()` | `useLoader(Loader)` at the read site, under `<Suspense>` |
87
- | `shouldRevalidate` (runs client) | `revalidate()` predicate — ALSO runs in the browser |
87
+ | `shouldRevalidate` (runs client) | `revalidate()` predicate — runs on the server |
88
88
  | `meta({ data })` | `ctx.use(Meta)` push from the loader body |
89
89
  | `defer` / `Await` | loaders stream; `<Suspense>` above each read |
90
90
 
91
- Note the `shouldRevalidate` row: a server-tree `revalidate()` runs on the
92
- server, but a `clientUrls()` predicate runs in the browser with
93
- client-computable args — the exact RR semantics. A group ported this way can
94
- still be re-migrated to server handlers later, route by route.
91
+ Note the `shouldRevalidate` row: RR runs it in the browser, but a
92
+ `clientUrls()` loader's predicate runs on the server like a server-tree
93
+ `revalidate()`, with the same arguments (`actionResult` and `formData`
94
+ included). The build moves it out of the `"use client"` module, so it may
95
+ reference imports and globals only: move a predicate that captures
96
+ module-local values into its own module and import it. A group ported this
97
+ way can still be re-migrated to server handlers later, route by route.
95
98
 
96
99
  Start with server handlers; reach for `clientUrls()` when a route group's
97
100
  conversion cost is dominated by rewriting interactive components rather than
98
101
  by moving data fetching — or when the group is a **high-navigation-speed
99
102
  surface** (dashboard, admin panel, settings): browser-local matching gives
100
- instant optimistic pending, and browser-run predicates hold data across
101
- tab/param switches, so transitions are the fastest Rango offers. See
103
+ instant optimistic pending, and per-loader `revalidate()` predicates (run on
104
+ the server) hold data across tab/param switches, so transitions are the fastest Rango offers. See
102
105
  `/client-urls`.
103
106
 
104
107
  ## Migration Strategy
@@ -55,7 +55,7 @@ Use it only when you need capabilities beyond what the handler provides:
55
55
  If the React Router loader just fetches data for its page component AND the
56
56
  component can become a server component, merge it into the handler. If the
57
57
  component stays a client component, port the whole group with `clientUrls()`
58
- instead — loader, `useLoader` read, and browser-run `revalidate()` keep the RR
58
+ instead — loader, `useLoader` read, and per-loader `revalidate()` keep the RR
59
59
  route-module shape (see the "Two target shapes" section in the main skill and
60
60
  `/client-urls`). See `/loader` for when the live data layer is useful.
61
61
 
@@ -149,6 +149,15 @@ material is shared per host+URL and request partition. `cache(false)`, or a
149
149
  `condition()` that returns false, on a ppr route now renders the request like a
150
150
  cache miss (no shell).
151
151
 
152
+ ### A request middleware rewrote
153
+
154
+ A request middleware rewrote (`throw rewrite()`, `/middleware`) to a `ppr`
155
+ route is served the target's shell under the target's key. The capture
156
+ renders the target as a direct visit: `ctx.rewrittenFrom` reads `undefined`
157
+ there and the payload carries the target's pathname, so the shell is the
158
+ same for direct and rewritten visitors (a component that renders
159
+ `usePathname()` shows the target's URL to a rewritten visitor on a HIT).
160
+
152
161
  ### Request-partitioned shells
153
162
 
154
163
  A route whose `cache()` record is partitioned by the request partitions its
@@ -1069,6 +1078,19 @@ Four hard edges (each e2e/unit-pinned):
1069
1078
  must be fresh on every serve, it belongs on the live lane (no `ssr: false`) or
1070
1079
  in a nested promise.
1071
1080
 
1081
+ ### `prefetch: false` on a `ppr` route
1082
+
1083
+ A `ppr` route's handler output is stored, so a `loading()` flagged with
1084
+ `prefetch: false` does not skip the handler in a `<Link>` prefetch: the shell
1085
+ and its partial replay are served as usual and only the live loaders behind
1086
+ the fallback are deferred. A live loader registered with `{ prefetch: false }` is
1087
+ skipped the same way, and the click that adopts the prefetch sends one fill
1088
+ request for it. An `ssr: false` loader is the bake lane and is served from the
1089
+ shell, so on a `ppr` route `prefetch: false` is ignored for it, and
1090
+ development logs a warning that the loader "is registered with ssr: false
1091
+ and prefetch: false on a ppr route"; drop one of the two options. See
1092
+ `/loader` → "`prefetch: false`".
1093
+
1072
1094
  ### The layout-with-loaders playbook (the storefront case)
1073
1095
 
1074
1096
  The most common real-app shape: an app-wide layout registers per-user loaders
@@ -666,6 +666,16 @@ build-time shell capture runs it and bakes its settled data into the shell,
666
666
  and a shell HIT runs it only when its return carries promises (`/ppr` → "The
667
667
  bake lane"). The Flight payload collection itself still skips loaders.
668
668
 
669
+ ### `prefetch: false` skips only the loaders
670
+
671
+ A `Prerender` handler's output is stored, so a `loading()` flagged with
672
+ `prefetch: false` on the route does not skip the handler in a `<Link>` prefetch: the
673
+ stored output is served as usual, and only the loaders behind the fallback are
674
+ deferred (a loader registered with `{ prefetch: false }` likewise). The click
675
+ that adopts the prefetch shows the fallback and sends one fill request for
676
+ them. This holds in development, where there is no build artifact, as in
677
+ production. See `/loader` → "`prefetch: false`".
678
+
669
679
  ### Build-time handle data is frozen
670
680
 
671
681
  Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
@@ -1283,8 +1293,8 @@ fresh.
1283
1293
  A warm replaces the entry where it runs, and on a shared store that is the
1284
1294
  entry other locations read on their next miss. It does not reach a copy
1285
1295
  another location already holds: on `CFCacheStore`, each colo keeps serving its
1286
- own Cache API copy until it expires, and an entry whose `ttl + swr` is under
1287
- 60 seconds never reaches KV at all.
1296
+ own Cache API copy until it expires or a tag invalidation rejects it, and an
1297
+ entry whose `ttl + swr` is under 60 seconds never reaches KV at all.
1288
1298
 
1289
1299
  So after a deploy (new keys: no location holds a copy) a warm alone makes
1290
1300
  the route ready everywhere. For a content change, invalidate first, then
@@ -1306,12 +1316,16 @@ path.json("/hooks/product-changed", async (ctx) => {
1306
1316
 
1307
1317
  The invalidation makes every location's copy unservable, so no visitor is
1308
1318
  served the old content, and the warm writes the new entry where it runs and
1309
- to the shared store. A location with no copy of its own reads that entry on
1310
- its next request and renders nothing. On `CFCacheStore`, a colo that was
1311
- still holding its own (now invalidated) copy renders once for itself instead
1312
- of reading the warmed entry, unless the store uses `tagPurge`, which evicts
1313
- those copies. Await `updateTag()` before the warm: a write that started
1314
- before an invalidation of one of its tags is refused.
1319
+ to the shared store. Every other location reads that entry on its next
1320
+ request and renders nothing: a location with no copy of its own, and, on
1321
+ `CFCacheStore` with `kvFallThrough: true` (default false), a colo that was
1322
+ still holding its own (now invalidated) copy, which reads KV as if it held
1323
+ none. Without the option such a colo renders once for itself, unless the store
1324
+ uses `tagPurge`, which evicts those copies. With the option, a colo renders
1325
+ for itself only while KV still hands it the entry from before the invalidation,
1326
+ which it refuses: Cloudflare documents up to 60
1327
+ seconds for a KV write to spread. Await `updateTag()` before the warm: a
1328
+ write that started before an invalidation of one of its tags is refused.
1315
1329
 
1316
1330
  ### Limits worth knowing
1317
1331
 
@@ -168,13 +168,16 @@ stated, greppable contract.
168
168
  only applies when the enclosing cache recomputes; put a value in a loader if it
169
169
  must be fresher. See `/cache-guide` → "Combining Both".
170
170
  - **A predicate runs where its effect lands.** A server-tree `revalidate()`
171
- decides what the server re-sends, so it runs on the server; an `intercept({
171
+ and a `clientUrls()` loader's `revalidate()` decide what the server re-sends,
172
+ so they run on the server (the build hoists a group's predicate out of its
173
+ `"use client"` module and passes it the `urls()` arguments: `/client-urls`);
174
+ an `intercept({
172
175
  when })` selector decides what the server renders, so it runs on the server;
173
- a `clientUrls()` `revalidate()` and `transition({ when })` decide a browser
174
- commit, so they run in the browser. A browser predicate sees URLs, params,
175
- route names, history state and action data, never `get()`, `env` or handler
176
- state; in a server `urls()` it is written inline (the build hoists it into a
177
- client module) or imported from a `"use client"` module (`/view-transitions`).
176
+ `transition({ when })` decides a browser commit, so it runs in the browser.
177
+ A `transition({ when })` predicate sees URLs, params, route names, history
178
+ state and action data, never `get()`, `env` or handler state; in a server
179
+ `urls()` it is written inline (the build hoists it into a client module) or
180
+ imported from a `"use client"` module (`/view-transitions`).
178
181
  - **Vocabulary.** `revalidate()` keeps React Router's `shouldRevalidate`
179
182
  dialect (`currentUrl`/`nextUrl`, `currentParams`/`nextParams`). Navigation
180
183
  selectors (`intercept({ when })`, `transition({ when })`) use `from`/`to`
@@ -270,6 +273,9 @@ the `path()` re-run; layouts and parallels above the route are skipped.
270
273
  revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
271
274
  ```
272
275
 
276
+ The same `actionResult` idiom works in a `clientUrls()` group's loader
277
+ `revalidate()`, which runs on the server with these arguments.
278
+
273
279
  **The source is the source of truth.** Structure, types, and update policy are
274
280
  visible and local in the tree — read top-down, no hidden global model to hold in
275
281
  your head. A snippet earns its place only if, from the code alone, you can answer:
@@ -305,7 +311,7 @@ Grouped by concern — read when you need to…
305
311
  | `/layout` | Layouts that wrap child routes |
306
312
  | `/parallel` | Multi-column layouts and sidebars |
307
313
  | `/intercept` | Modal/slide-over patterns for soft navigation |
308
- | `/middleware` | Request processing and authentication |
314
+ | `/middleware` | Request processing, authentication, `throw rewrite()` (same-URL rewrites) |
309
315
  | `/host-router` | Multi-app host routing with domain/subdomain patterns |
310
316
  | `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
311
317
  | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |