@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -1,42 +1,98 @@
1
1
  ---
2
2
  name: cache-guide
3
- description: When to use cache() DSL vs "use cache" directive — key differences and decision guide. Use when unsure which caching mechanism fits a given problem, comparing route/segment caching, function-level caching, and document-level caching, or asking "which cache API should I use".
3
+ description: Decision guide for Rango's cache layers — "use cache", loader cache(), segment cache(), Prerender/Static, the ppr HTML shell, and document/CDN caching — with the cache() vs "use cache" comparison (keys, guards, SWR, nesting). Use when unsure which caching mechanism fits a problem, comparing layers, or asking "which cache API should I use".
4
4
  argument-hint:
5
5
  ---
6
6
 
7
- # cache() vs "use cache" — When to Use Which
8
-
9
- Both mechanisms share the same backing store and cache profiles, and both accept
10
- an optional `tags` field (honored by the built-in stores — invalidate with
11
- `updateTag`/`revalidateTag`; see "Two axes" below). They differ in scope, cache
12
- key, execution model, and runtime control.
7
+ # Choosing a cache layer
8
+
9
+ Start here when you are not sure which cache to use. The first section maps
10
+ every layer; the rest of the page compares the two you will reach for most,
11
+ the `cache()` DSL and the `"use cache"` directive.
12
+
13
+ ## The layers at a glance
14
+
15
+ Each layer stores a more "cooked" artifact than the one above it, and keeps less
16
+ of the request live on a hit. The runtime layers share the app-level store
17
+ (`createRouter({ cache })`) and one tag system (`cacheTag` / `updateTag` /
18
+ `revalidateTag`); `Prerender()`/`Static()` output is built into the server
19
+ bundle instead.
20
+
21
+ | Layer | Declared with | Stores | Still runs on a hit | Skill |
22
+ | -------------------- | ---------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | --------------------- |
23
+ | Function / component | `"use cache"` | one function's return value | everything around the call | `/use-cache` |
24
+ | Loader data | `loader(L, () => [cache({...})])` | one loader's result | other loaders, handlers, rendering | `/caching`, `/loader` |
25
+ | Segments, runtime | `cache({...}, () => [...])` | rendered Flight segments of a subtree | middleware, segments above it, loaders, HTML render | `/caching` |
26
+ | Segments, build time | `Prerender()` / `Static()` | Flight segments rendered at build | middleware, loaders, HTML render | `/prerender` |
27
+ | HTML shell | `ppr` path option | HTML prelude + React postponed state + the handler layer | middleware and live loaders (no handler); holes resume | `/ppr` |
28
+ | Whole response (app) | `createDocumentCacheMiddleware()` + `s-maxage` | final response in the app store | middleware above it; nothing below | `/document-cache` |
29
+ | Whole response (CDN) | `Cache-Control: s-maxage` read by the platform | final response outside the app | nothing — the app is not invoked | `/deployment-caching` |
30
+
31
+ Quick rules:
32
+
33
+ - Expensive **data** used in several places → `"use cache"` on the fetch.
34
+ - Expensive **rendering** of a route subtree → `cache()`.
35
+ - Content fixed at **build time** (known params, files on disk) → `Prerender()`.
36
+ - **Instant first byte** of HTML while parts stay per-request → `ppr`.
37
+ - **Whole response** public and identical for everyone → `/document-cache` or
38
+ CDN caching (`/deployment-caching`).
39
+ - A value that must be fresh on **every** request → a loader (never cached
40
+ unless you opt in), read with `useLoader()`; on a `ppr` route, keep it off
41
+ `ssr: false`. A handler inside a `cache()` or `ppr` boundary that awaits
42
+ `ctx.use(Loader)` renders the value it got on the miss, and every hit
43
+ replays that output.
44
+
45
+ The layers compose: `"use cache"` inside a `cache()` subtree, a cached loader
46
+ under a `cache()` boundary, `cache()` segments replayed inside a `ppr` shell
47
+ capture, or `Prerender()` + `ppr` for a shell baked at build time.
48
+
49
+ ## cache() vs "use cache"
50
+
51
+ Both mechanisms use the same app-level store, and both can be tagged
52
+ (`cache({ tags })`; profile `tags` or runtime `cacheTag()` for `"use cache"`) and
53
+ invalidated with `updateTag`/`revalidateTag` — see "Two axes" below. Named
54
+ cache profiles apply to `"use cache"` only. They differ in scope, cache key,
55
+ execution model, and runtime control.
13
56
 
14
57
  ## Two axes — do not conflate
15
58
 
16
- Everything on this page is **axis 1: stored-value freshness** — _is a cached
17
- value still good?_ There is a second, orthogonal axis it is easy to mistake for
59
+ Everything on this page is **stored-value freshness** — _is a cached value
60
+ still good?_ There is a second, orthogonal concern it is easy to mistake for
18
61
  caching:
19
62
 
20
- 1. **Stored-value freshness** — _is a cached value still good?_
21
- → `"use cache"` (fn/component), `cache()` (segment), loader `cache()` (loader data).
22
- Entries expire by **TTL/SWR** and can be tagged (`cache({ tags })` or runtime
23
- `cacheTag(...tags)` — inside `"use cache"` it tags that entry; called during a
24
- request render outside `"use cache"` it tags the document/shell artifact).
25
- Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`)
26
- index by tag; invalidate on demand with `updateTag(...tags)` (awaitable,
27
- read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking).
28
- Both hard-purge; the difference is awaitability, not stale-serving.
63
+ 1. **Stored-value freshness** — _is a cached value still good?_ → `"use cache"`
64
+ (fn/component), `cache()` (segment), loader `cache()` (loader data). Entries
65
+ expire by **TTL/SWR** and can be tagged (`cache({ tags })` or runtime
66
+ `cacheTag(...tags)` — inside `"use cache"` it tags that entry; called during
67
+ a request render outside `"use cache"` it tags what the render stores: the
68
+ enclosing route `cache()` entry, a cached loader's own entry when called in
69
+ its body, and the `ppr` shell or document-cache entry built from them; on a
70
+ `ppr` route it tags the shell only when shell material calls it). All
71
+ built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`,
72
+ `VercelCacheStore`) index by tag; invalidate on demand with
73
+ `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)` (background,
74
+ non-blocking). Both evict rather than mark stale, and the request that calls
75
+ either reads its own writes; `updateTag()` also waits for the durable write,
76
+ `revalidateTag()` leaves it in the background. `CFCacheStore` and
77
+ `VercelCacheStore` serve PPR shell reads through per-isolate memos: the
78
+ invalidating user skips them via the fresh-reads cookie, other users see the
79
+ invalidation once the memo refreshes and, on `CFCacheStore` with KV, once KV
80
+ propagates the marker to their colo (`/caching` → "Tag-Based Invalidation"
81
+ and "The fresh-reads cookie").
29
82
  2. **Client-update selection** — _should this segment re-run and stream to the
30
83
  client on this navigation/action?_
31
84
  → `revalidate()`. Covered in `/loader` and `/route`, **not here**.
32
85
 
33
86
  They are orthogonal and compose: a segment selected by `revalidate()` still
34
- consults its cache (hit → no recompute); a cache bust does **not** force a client
87
+ consults its cache (hit → no recompute), except that a server action's
88
+ revalidation render skips the route `cache()` lookup and re-renders those
89
+ segments fresh (a loader's own `cache()` and `"use cache"` are still read); a
90
+ cache bust does **not** force a client
35
91
  update, and `revalidate()` never reads, writes, or expires a cached value. If you
36
92
  know React Router, `revalidate()` is `shouldRevalidate`, not `Cache-Control`. See
37
93
  `/rango` → "Coming from another framework" for the cross-framework mapping.
38
94
 
39
- ## Fast choice
95
+ ## Fast choice: cache() or "use cache"
40
96
 
41
97
  Read this first; use the rest of the page when the choice has edge cases.
42
98
 
@@ -62,20 +118,35 @@ protection the framework already gives you (or assume one it deliberately
62
118
  doesn't).
63
119
 
64
120
  There are two guard models to keep separate. Both block response side effects
65
- (`ctx.header()`, cookie writes) that would be lost on a hit; they differ in what
121
+ (`ctx.headers.set()`, cookie writes) that would be lost on a hit; they differ in what
66
122
  else they allow:
67
123
 
68
124
  - **`cache()` boundary guard** (route-level) — fires while the handler runs on a
69
125
  miss. `cookies()` and `headers()` throw (request-scoped data would be baked into
70
126
  the shared cached shell), `ctx.get(nonCacheableVar)` throws (a tainted value
71
- would be baked in), and response side effects (`ctx.header()`, `ctx.setCookie()`,
72
- `ctx.setStatus()`, `ctx.onResponse()`) throw. `ctx.set()` of a cacheable var is
127
+ would be baked in), and response side effects (`ctx.headers.set()`,
128
+ `setCookie()`, `setStatus()`, `onResponse()`) throw. `ctx.set()` of a cacheable var is
73
129
  **allowed** — children are cached too and can read it. **Loaders are exempt**
74
- (they always run fresh) — read request data inside a loader.
130
+ (a route `cache()` does not store their values) — read request data inside a
131
+ loader.
132
+ - **Loader `cache()` guard** (a loader bound with its own `cache()`) — its value
133
+ is stored under a key that names no user, so a miss whose body read
134
+ `cookies()`, `headers()`, the theme or a non-cacheable `ctx.get()` fails
135
+ unless the binding has a `key()` or its store a `keyGenerator`. Either one
136
+ switches the check off, so put what the body reads in it (`/loader` →
137
+ "Cache Key").
75
138
  - **`"use cache"` exec-guard** (function-level) — the same request-scoped APIs
76
139
  throw inside the cached function (`cookies()`, `headers()`, `ctx.set()`,
77
- `ctx.header()`); additionally, tainted `ctx`/`env`/`req` args are excluded from
78
- the cache key.
140
+ `ctx.headers.set()` and other response writes); additionally, tainted
141
+ `ctx`/`env`/`req` args are excluded from the cache key. The guard runs only on
142
+ the cached path: with no item-capable store configured the function runs
143
+ uncached and nothing throws.
144
+
145
+ The `ppr` shell capture has its own, stricter guard: `cookies()`, `headers()`, a
146
+ theme read, a `{ cache: false }` variable read, and `ctx.dynamic()` refuse the
147
+ capture anywhere it waits — handlers, promises they pass or push, async server
148
+ components, `ssr: false` loaders, and loaders a handler awaits (no loader
149
+ exemption there). The route then serves uncached; see `/ppr`.
79
150
 
80
151
  ### Cross-deploy safety: version-segmented store keys
81
152
 
@@ -94,7 +165,9 @@ deliberately: accept it (correctness over hit-rate), or split the policy — let
94
165
  the render/edge cache auto-version while a separate data store gets a stable
95
166
  `version` so its entries survive deploys. (Per-process stores like
96
167
  `MemorySegmentCacheStore` are cold on every restart anyway; this matters for
97
- persistent stores.) See `/caching` for store setup.
168
+ persistent stores.) `VercelCacheStore` does not version automatically: pass a
169
+ per-deploy `getCache({ namespace })` or its `version` option (see `/vercel`).
170
+ See `/caching` for store setup.
98
171
 
99
172
  ### Client cache: forward/back is mutation-aware
100
173
 
@@ -110,9 +183,10 @@ solved on the partial-render axis.
110
183
 
111
184
  `createVar({ cache: false })` (or a `ctx.set(var, v, { cache: false })` write)
112
185
  taints a value as request-scoped; reading it **directly** with `ctx.get()` inside
113
- a `cache()` boundary throws — the guard against the catastrophic "serve user A's
114
- data to user B" bug. The guarantee is precise and intentionally narrow — see
115
- "Context Variable Cache Safety" below for exactly what it does and does not catch.
186
+ a `cache()` boundary or a `"use cache"` function throws — the guard against the
187
+ catastrophic "serve user A's data to user B" bug. The guarantee is precise and
188
+ intentionally narrow — see "Context Variable Cache Safety" below for exactly
189
+ what it does and does not catch.
116
190
 
117
191
  ## Stale-while-revalidate
118
192
 
@@ -139,40 +213,58 @@ recompute for a merely-aging entry.
139
213
  SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
140
214
  has no stale entry to serve (see version-segmented store keys above).
141
215
 
142
- Store support is layer-specific. `CFCacheStore` supports SWR for segment,
143
- document/response, and `"use cache"` item entries. `MemorySegmentCacheStore`
144
- supports SWR for response and `"use cache"` item entries, but its route-segment
145
- entries expire at TTL and never background-revalidate. Use the memory store for
216
+ Store support is layer-specific. `CFCacheStore` and `VercelCacheStore` support
217
+ SWR for segment, document/response, item (`"use cache"` and cached loaders), and
218
+ PPR shell entries. `MemorySegmentCacheStore` supports SWR for response, item,
219
+ and shell entries, but its route-segment entries expire at `ttl` (the `swr`
220
+ window is dropped) and never background-revalidate. Use the memory store for
146
221
  local/dev behavior, not as proof that segment SWR is active.
147
222
 
148
223
  ## Key Differences
149
224
 
150
- | | `cache()` DSL | `"use cache"` directive |
151
- | -------------------- | ----------------------------------------------------- | -------------------------------------------------- |
152
- | **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
153
- | **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
154
- | **Cache key** | Request type + pathname + params (+ optional custom) | Function identity + serialized non-tainted args |
155
- | **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
156
- | **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
157
- | **Side effects** | Response side effects throw inside the boundary | `ctx.header()`, `ctx.set()`, etc. throw at runtime |
158
- | **Handle data** | Captured and replayed | Captured and replayed |
159
- | **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
160
- | **Nesting** | Nest `cache()` boundaries with different TTLs | Compose by calling cached functions from uncached |
225
+ | | `cache()` DSL | `"use cache"` directive |
226
+ | -------------------- | ----------------------------------------------------- | ------------------------------------------------- |
227
+ | **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
228
+ | **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
229
+ | **Cache key** | Request type + host + pathname + params + search | Function identity + serialized non-tainted args |
230
+ | **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
231
+ | **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
232
+ | **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
233
+ | **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
234
+ | **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
235
+ | **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached |
161
236
 
162
237
  ### cache() Cache Key
163
238
 
164
- The key is `{requestType}:{pathname}:{params}` where requestType is one of
165
- `doc:`, `partial:`, or `intercept:`. This means the same URL cached separately
166
- for full document loads, client navigations, and intercept navigations.
167
-
168
- Custom `key` functions can segment the cache further (e.g., by user role or locale).
169
- `condition` can disable caching entirely at runtime (e.g., skip for authenticated users).
239
+ The default key is `{requestType}:{host}{pathname}[:params][?search]`, where
240
+ requestType is `doc`, `partial`, or `intercept`. The same URL is therefore cached
241
+ separately for full document loads, client navigations, and intercept
242
+ navigations. The host keeps tenants apart when one deployment serves several
243
+ domains; the search part is sorted, excludes the router's internal params, and
244
+ honors `createRouter({ cache: { searchParams } })`.
245
+
246
+ A custom `key` function replaces the whole default key (e.g., to key by user role
247
+ or locale); it also bypasses the store's `keyGenerator` and the search-param
248
+ filter. It runs once per request and may read `cookies()`. A nested `cache()`
249
+ keys its records within the enclosing `key()` partition (`/caching` → "Keys
250
+ nest"). Never return raw request input from `key()`: normalize it to the
251
+ values you serve or prefix it. On a `ppr` route, `key()` (and a store
252
+ `keyGenerator` that returns a non-default key) also partitions the shell: one
253
+ shell per value, and a partitioned request never reads a build-time shell.
254
+ Keep the values to a small set. `condition` can disable caching entirely at
255
+ runtime (e.g., skip for authenticated users).
170
256
 
171
257
  ### "use cache" Cache Key
172
258
 
173
259
  The key is `use-cache:{functionId}:{serializedArgs}` where functionId is a stable
174
- ID from the Vite transform (module path + export name) and args are serialized via
175
- RSC `encodeReply()`. Tainted arguments (ctx, env, req) are excluded.
260
+ ID from the Vite transform (module path + export name) and args are serialized
261
+ (stable JSON when every arg is JSON-safe, RSC `encodeReply()` otherwise).
262
+ Request-scoped arguments are excluded: route fields read off `ctx` (handler,
263
+ loader or middleware: host, route name, pathname, params, search) and a
264
+ `Request`'s URL are folded in, and `env`
265
+ is left out. React element slots stay out of the key; any other argument that
266
+ cannot be serialized (a function, a class instance) runs the call uncached. See
267
+ `/use-cache`.
176
268
 
177
269
  ## Execution Model
178
270
 
@@ -180,17 +272,22 @@ This is the most important distinction.
180
272
 
181
273
  ### cache() — all-or-nothing
182
274
 
183
- On cache hit, the cache-lookup middleware short-circuits the entire pipeline.
184
- No handler code runs. On miss, all handlers execute normally and segments are
185
- stored.
275
+ On cache hit, the cache-lookup middleware short-circuits segment resolution for
276
+ the boundary: no handler inside it runs. Segments above the boundary are not in
277
+ the entry and resolve as on an uncached render. On miss, all handlers execute
278
+ normally and the boundary's segments are stored.
186
279
 
187
280
  ```
188
- HIT → cached segments served, loaders resolved fresh, no handler runs
189
- MISS → all handlers run, segments cached, response built normally
281
+ HIT → segments above the boundary resolved, cached segments served, loaders resolved fresh, no handler in the boundary runs
282
+ MISS → all handlers run, the boundary's segments cached, response built normally
190
283
  ```
191
284
 
192
- Headers, cookies, and ctx.set() calls inside handlers naturally don't execute on
193
- hit. There is no partial execution, so no runtime guards are needed.
285
+ `ctx.set()` calls are safe: every handler that could read the value is inside the
286
+ same cached unit, so a hit replays a consistent subtree. Response side effects
287
+ and request-scoped reads are not safe — a write would reach only MISS responses
288
+ and a `cookies()` read would be baked into the shared entry — so the boundary
289
+ guard throws on them even on a miss (see "Correctness & invalidation" above and
290
+ "Headers and Cookies" below).
194
291
 
195
292
  ### "use cache" — partial execution
196
293
 
@@ -203,11 +300,13 @@ HIT → function body skipped, calling code runs, handle data replayed
203
300
  MISS → function body runs, return value + handle data cached
204
301
  ```
205
302
 
206
- Runtime guards throw if you call cookies(), headers(), ctx.header(), ctx.set(),
207
- ctx.onResponse(), ctx.setTheme(), or ctx.setLocationState() inside a "use cache"
208
- function. cookies() and headers() are blocked because per-request data is not in the
209
- cache key. Side-effect methods are blocked because their effects are lost on hit.
210
- Use ctx.use(Handle) instead for data — handle data is captured and replayed.
303
+ Runtime guards throw if you call `cookies()`, `headers()`, `ctx.set()`,
304
+ `ctx.headers.set()` (or any response write: cookie writes, `setStatus()`,
305
+ `onResponse()`), `ctx.setTheme()`, or `ctx.setLocationState()` inside a
306
+ `"use cache"` function. `cookies()` and `headers()` are blocked because
307
+ per-request data is not in the cache key. Side-effect methods are blocked because
308
+ their effects are lost on hit. Use `ctx.use(Handle)` instead for data — handle
309
+ data is captured and replayed.
211
310
 
212
311
  ## When to Use cache()
213
312
 
@@ -230,6 +329,11 @@ export const urlpatterns = urls(({ path, cache }) => [
230
329
  ]);
231
330
  ```
232
331
 
332
+ On a `ppr` route, `cache(false)` or a `condition()` that returns false also
333
+ means no shell: that route (or that request) renders like a cache miss, with
334
+ no shell served or captured. A `ppr` route cannot opt out of a layout's
335
+ `cache()` and still get a shell.
336
+
233
337
  ## When to Use "use cache"
234
338
 
235
339
  Use the `"use cache"` directive when:
@@ -250,10 +354,11 @@ async function getProductData(slug: string) {
250
354
  return await db.query("SELECT * FROM products WHERE slug = ?", [slug]);
251
355
  }
252
356
 
253
- // Handler calls cached function, sets headers outside
357
+ // Handler calls cached function, sets headers outside it
358
+ // (this route is not inside a cache() boundary or a ppr route)
254
359
  async function ProductPage(ctx) {
255
360
  const data = await getProductData(ctx.params.slug);
256
- ctx.header("X-Product", data.id);
361
+ ctx.headers.set("X-Product", data.id);
257
362
  return <Product data={data} />;
258
363
  }
259
364
  ```
@@ -299,21 +404,39 @@ does **not** keep a value fresher than its parent:
299
404
  So `"use cache: short"` (60s) inside `cache({ ttl: 600 })` yields ~600s freshness
300
405
  on hits, **not** 60s. This is not a bug: setting `cache({ ttl: 600 })` declares
301
406
  "this subtree may be ~600s stale." **If a value must be fresher than its
302
- enclosing segment, put it in a loader** (always live). `debugPerformance` prints
303
- cache hits per layer, so the actual per-request behavior is observable.
407
+ enclosing segment, put it in a loader** read with `useLoader()` (without
408
+ `ssr: false` on a `ppr` route). `debugPerformance` prints cache hits per
409
+ layer, so the actual per-request behavior is observable.
410
+
411
+ Keys and tags cross the nesting too:
412
+
413
+ - An inner `cache()` without `key` keys its records within the outer `key()`
414
+ partition; one with its own `key` composes both (`/caching` → "Keys nest").
415
+ - Tags that `cacheTag()` calls and `"use cache"` reads record inside a
416
+ `cache()` subtree are stored on that `cache()` entry, so `updateTag()` of one
417
+ of them evicts it (and a `ppr` shell or document built from it) instead of
418
+ waiting for it to expire.
419
+ - A `ppr` shell never outlives the route `cache()` entry it was captured
420
+ from: its ttl and ttl + swr are capped to what that entry has left.
304
421
 
305
422
  ## Headers and Cookies
306
423
 
307
424
  Neither mechanism caches response headers or cookies.
308
425
 
309
426
  - **cache()**: Response-level side effects throw inside the cache boundary even
310
- on a miss: `ctx.header()`, `ctx.setCookie()`, `ctx.deleteCookie()`,
311
- `ctx.setStatus()`, `ctx.onResponse()`, and direct `ctx.headers` mutation. On a
312
- hit the handler would be skipped, so allowing the write on a miss would produce
313
- inconsistent responses. If you need headers or cookies on every response, set
314
- them in middleware or a live segment outside the cache boundary.
315
- - **"use cache"**: cookies() and headers() throw inside the cached function
316
- (both reads and writes). ctx.header() also throws. Move them outside.
427
+ on a miss: `ctx.headers` mutation (`ctx.headers.set()` etc.), `cookies()`
428
+ (read or write), and the request-context writers `header()`, `setCookie()`,
429
+ `deleteCookie()`, `setStatus()`, `onResponse()`. On a hit the handler would be
430
+ skipped, so allowing the write on a miss would produce inconsistent responses.
431
+ Registered loaders are exempt (they run on every request, hits included). If
432
+ you need headers or cookies on every response, set them in middleware, a live
433
+ segment outside the cache boundary, or a loader.
434
+ - **"use cache"**: `cookies()` and `headers()` throw inside the cached function
435
+ (both reads and writes), and so do `ctx.headers` mutations. Move them outside.
436
+ - **`ctx.theme`** (handler and middleware) and **`getRequestContext().theme`**
437
+ are the visitor's theme cookie, so a read throws in both scopes, like
438
+ `cookies()`. Read it with `useTheme()` in a client component, or in a
439
+ loader (see the `/theme` skill).
317
440
 
318
441
  ```typescript
319
442
  // Set headers that must appear on every response in middleware
@@ -327,8 +450,9 @@ middleware(async (ctx, next) => {
327
450
 
328
451
  Context variables created with `createVar()` are cacheable by default and can
329
452
  be read freely inside cached scopes. A non-cacheable var throws when read
330
- **directly** with `ctx.get()` inside a `cache()` boundary — where the value would
331
- otherwise be serialized into the stored segment.
453
+ **directly** with `ctx.get()` inside a `cache()` boundary or a `"use cache"`
454
+ function — where the value would otherwise be serialized into the stored
455
+ segment or entry without being part of its key.
332
456
 
333
457
  There are two ways to mark a value as non-cacheable:
334
458
 
@@ -345,30 +469,36 @@ specifies `cache: false`, the value is non-cacheable.
345
469
 
346
470
  **Behavior inside a `cache()` boundary:**
347
471
 
348
- | Operation | Inside a `cache()` boundary |
349
- | ----------------------------------------- | ------------------------------------------------------ |
350
- | `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
351
- | `ctx.get(cacheableVar)` | Allowed |
352
- | `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
353
- | `ctx.set(var, value)` (cacheable) | Allowed |
354
- | `ctx.header()` / cookie writes | Throws (response side effect would be lost on hit) |
355
- | Any of the above **inside a loader** | Allowed (loaders always run fresh) |
472
+ | Operation | Inside a `cache()` boundary |
473
+ | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
474
+ | `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
475
+ | `ctx.theme`, `getRequestContext().theme` | Throws, like `cookies()` (#971) |
476
+ | `ctx.get(cacheableVar)` | Allowed |
477
+ | `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
478
+ | `ctx.set(var, value)` (cacheable) | Allowed |
479
+ | `ctx.headers.set()` / cookie writes | Throws (response side effect would be lost on hit) |
480
+ | Any of the above **inside a loader** | Allowed (loaders always run fresh) |
481
+ | The reads above in a loader with its own `cache()` | The miss fails without a `key()`; any `key()` allows them, so it must include them |
356
482
 
357
483
  (Both scopes block the same request-scoped APIs — `cookies()`, `headers()`,
358
484
  response side effects, and non-cacheable `ctx.get()` — because each would leak
359
485
  per-request data into a shared cache entry. The `cache()` boundary tracks the
360
486
  scope via `isInsideCacheScope()`; `"use cache"` uses the exec guard and also
361
487
  excludes tainted `ctx`/`env`/`req` args from the cache key. Loaders are exempt in
362
- both — see "Headers and Cookies" and the precise guarantee below.)
488
+ both — see "Headers and Cookies" and the precise guarantee below. Under `"use
489
+ cache"` the exemption covers a loader body the cached function consumes
490
+ (`await ctx.use(Loader)`), not a cached function a loader calls: the loader
491
+ re-runs on every request, the cached body does not.)
363
492
 
364
493
  Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
365
494
  Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
366
495
  scope and rejects non-cacheable reads.
367
496
 
368
- ### The guarantee is precise — a direct read inside `cache()`, not propagating
497
+ ### The guarantee is precise — a direct read inside a cache scope, not propagating
369
498
 
370
499
  The guard fires on a **direct** `ctx.get(taintedVar)` **inside a `cache()`
371
- boundary** (the scope `isInsideCacheScope` detects). The taint lives on the
500
+ boundary** (the scope `isInsideCacheScope` detects) **or a `"use cache"`
501
+ function body**. The taint lives on the
372
502
  variable; a value **derived** from it and read **outside** the boundary is not
373
503
  tracked:
374
504
 
@@ -396,35 +526,47 @@ layout((ctx) => {
396
526
  So do **not** read this as "you can't cache user data" — that overstates it and
397
527
  breeds the false confidence that makes the derived leak _more_ likely. The guard
398
528
  is deliberately non-propagating (propagation would cost a wrapper per derivation
399
- on the hot path), and it is scoped to the `cache()` segment boundary. `"use
400
- cache"` functions block the same request-scoped reads (`cookies()` / `headers()`
401
- throw inside them) and additionally exclude tainted `ctx`/`env`/`req` args from
402
- the cache key. The pattern that stays safe is also the natural one:
529
+ on the hot path), and it is scoped to the `cache()` segment boundary and the
530
+ `"use cache"` body. `"use cache"` functions block the same request-scoped reads
531
+ (`cookies()` / `headers()` and non-cacheable `ctx.get()` throw inside them) and
532
+ additionally exclude tainted `ctx`/`env`/`req` args from the cache key — pass a
533
+ non-cacheable value in as an argument so it becomes part of the key. The pattern that stays safe is also the natural one:
403
534
  **read tainted context at the point of use, in the path that needs it (a loader or
404
535
  live segment) — never extract user data into a plain value and cache that.**
405
536
  Loaders are exempt because they run outside the cache scope and resolve fresh
406
- every request.
537
+ every request. A loader with its own `cache()` does not: its value is stored,
538
+ so it falls under the loader `cache()` guard above.
407
539
 
408
540
  ## Loaders Are Always Fresh
409
541
 
410
- Loaders are **never cached** by route-level `cache()`. Even on a full cache hit
411
- where all UI segments are served from cache, loaders are re-resolved fresh on
412
- every request. This is enforced at two levels:
413
-
414
- 1. **Storage**: `cacheRoute()` filters out loader segments before serialization
415
- (`segments.filter(s => s.type !== "loader")`).
416
- 2. **Retrieval**: On cache hit, `resolveLoadersOnly()` runs after yielding cached
417
- UI segments, ensuring fresh data regardless of cache state.
418
-
419
- This means `cache()` gives you cached UI + fresh data by default. To also cache
420
- a loader's data, explicitly opt in with `loader(Fn, () => [cache({...})])`.
542
+ Loaders are **never cached** by route-level `cache()`. Even on a cache hit
543
+ where the boundary's UI segments are served from cache, loaders are re-resolved
544
+ fresh on every request. This is enforced at two levels:
545
+
546
+ 1. **Storage**: `cacheRoute()` filters out loader segments before serialization.
547
+ 2. **Retrieval**: On cache hit, `resolveLoadersOnly()` runs the boundary's
548
+ loaders after yielding its cached UI segments (the loaders above the boundary
549
+ run with their live segments), ensuring fresh data regardless of cache state.
550
+
551
+ This means `cache()` gives you cached UI + fresh data by default, where the
552
+ data is read outside the cached output: `useLoader()` in a client component.
553
+ A handler inside the boundary that awaits `ctx.use(Loader)` renders the value
554
+ it got on the miss, and every hit replays that output. On a `ppr` route an
555
+ `ssr: false` loader is the other exception: it bakes into the shell (`/ppr`).
556
+ To also cache a loader's data, explicitly opt in with
557
+ `loader(Fn, () => [cache({...})])`.
558
+ That entry is keyed by loader, host, path and params, not by the route's
559
+ `key()`, so give it a `key()` that includes any request data its body reads;
560
+ without one the miss fails.
421
561
 
422
562
  ## cache() Placement Patterns
423
563
 
424
- ### Wrapping children of a path
564
+ ### cache() among a path's children
425
565
 
426
- An orphan `cache()` inside a path's children becomes the parent for all
427
- subsequent siblings. Everything below the cache boundary is cached as one unit:
566
+ A `cache()` among a path's children caches that path. The path's handler and
567
+ its own layouts and parallels are one cached unit, wherever the `cache()` sits
568
+ in the list. Every other item still attaches to the path, and the segments
569
+ above the path stay live:
428
570
 
429
571
  ```typescript
430
572
  path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
@@ -433,34 +575,66 @@ path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
433
575
  parallel("@stats", StatsPanel),
434
576
  parallel("@activity", ActivityFeed),
435
577
  ]),
578
+ loader(DashboardLoader),
436
579
  ]),
437
580
  ```
438
581
 
439
582
  On hit: DashboardPage, DashboardSidebar, StatsPanel, and ActivityFeed are all
440
- served from cache. On miss: all handlers run, all segments cached together.
583
+ served from cache, and DashboardLoader runs fresh. On miss: all handlers run and
584
+ the path's segments are cached together. DashboardPage runs inside the boundary,
585
+ so the guards apply to it: a header write there throws. The wrapper form
586
+ `cache({ ttl: 300 }, () => [layout(DashboardSidebar)])` inside a path does the
587
+ same, and `cache(false)` there opts the path out of an enclosing `cache()` (on a
588
+ `ppr` route, out of the shell too). The same form caches a response route:
589
+ `path.json("/api/feed", handler, { name: "feed" }, () => [cache({ ttl: 60 })])`.
590
+ A `cache()` inside a `layout()` or `middleware()` wrapper in the path caches the
591
+ path too: `layout(DashboardSidebar, () => [cache({ ttl: 300 })])` is the same
592
+ unit as the example above, not the sidebar alone.
593
+
594
+ ### Bare cache() among a layout's children
595
+
596
+ A `cache()` with no children callback covers the routes after it. The
597
+ `layout()`, `parallel()`, `loader()` and `middleware()` items after it still
598
+ wrap every route of the layout, including the routes before the `cache()`;
599
+ they are cached only with the routes after it:
600
+
601
+ ```typescript
602
+ layout(<AppShell />, () => [
603
+ path("/a", PageA, { name: "a" }), // PromoBanner renders live on /a
604
+ cache({ ttl: 60 }),
605
+ layout(<PromoBanner />),
606
+ path("/b", PageB, { name: "b" }), // PromoBanner cached with /b
607
+ ]),
608
+ ```
609
+
610
+ With no route after the `cache()`, nothing is cached. To cache
611
+ `<PromoBanner />` with PageA too, put the `cache()` before PageA's `path()`.
612
+ A `cache()` among PageA's own children does not cover it: the banner belongs
613
+ to the layout, above that path's boundary.
441
614
 
442
615
  ### Uncached layout with cached children
443
616
 
444
617
  The cache boundary only covers what's inside it. Parent segments above the
445
- boundary are not cached and always re-render:
618
+ boundary are not cached and always re-render, cache hits included:
446
619
 
447
620
  ```typescript
448
621
  layout(RootLayout, () => [
449
622
  // RootLayout is NOT cached — runs every request
450
- path("/products/:slug", ProductPage, { name: "product" }, () => [
451
- cache({ ttl: 300 }),
452
- layout(ProductSidebar),
453
- parallel("@reviews", ReviewsPanel),
454
- parallel("@related", RelatedProducts),
623
+ cache({ ttl: 300 }, () => [
624
+ path("/products/:slug", ProductPage, { name: "product" }, () => [
625
+ parallel("@reviews", ReviewsPanel),
626
+ parallel("@related", RelatedProducts),
627
+ ]),
455
628
  ]),
456
629
  ]),
457
630
  ```
458
631
 
459
- RootLayout renders fresh every request. ProductPage, ProductSidebar,
460
- ReviewsPanel, and RelatedProducts are all inside the cache boundary and
461
- served from cache on hit. This is useful when the root layout depends on
462
- request-specific data (user session, theme) but the product content is
463
- cacheable.
632
+ RootLayout renders fresh every request, and a header it writes lands on every
633
+ response. ProductPage, ReviewsPanel, and RelatedProducts are inside the cache
634
+ boundary and served from cache on hit. This is useful when the root layout
635
+ depends on request-specific data (user session, theme) but the product content
636
+ is cacheable. On a `ppr` route the shell bakes RootLayout too: a shell HIT runs
637
+ no layout, and the shell expires no later than this `cache()` entry.
464
638
 
465
639
  ### Loader-level caching
466
640
 
@@ -479,14 +653,22 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
479
653
  ```
480
654
 
481
655
  This attaches the cache config directly to the loader entry. The loader's
482
- data is cached independently from the route's segment cache. Loader caching
656
+ data is cached independently from the route's segment cache, together with
657
+ the handle pushes its body made (replayed on every hit). Loader caching
483
658
  supports custom keys, tags, SWR, conditional bypass, and per-loader store
484
- overrides — see `/loader` for the full reference.
659
+ overrides — see `/loader` for the full reference. A body that reads
660
+ `cookies()`, `headers()` or a non-cacheable variable needs a `key()`, which
661
+ must include the value: without one the miss fails, and any `key()` switches
662
+ that check off. The entry also carries the tags its body recorded
663
+ (`cacheTag()` calls, `"use cache"` reads, and those of the loaders it reads
664
+ with `ctx.use()`), so `updateTag()` of any of them drops it.
485
665
 
486
666
  ## See Also
487
667
 
488
- - `/caching` — cache() DSL setup, stores, nested boundaries
668
+ - `/caching` — cache() DSL setup, stores, tags, nested boundaries
489
669
  - `/use-cache` — "use cache" directive details, profiles, transforms, guards
670
+ - `/loader` — loader-level caching and the loader context
671
+ - `/prerender` — build-time segments with `Prerender()`/`Static()`
672
+ - `/ppr` — PPR shell caching: cached HTML shell + live holes (different layer)
490
673
  - `/document-cache` — store-backed complete-response middleware
491
674
  - `/deployment-caching` — in-function versus external CDN cache boundaries
492
- - `/ppr` — PPR shell caching: cached HTML shell + live loader holes (different layer)