@rangojs/router 0.16.0 → 0.17.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.
- package/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/skills/scripts/SKILL.md
CHANGED
|
@@ -6,33 +6,38 @@ argument-hint: "[vendor]"
|
|
|
6
6
|
|
|
7
7
|
# Scripts
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
Push a script config from a **server** route/layout handler (or a loader) with
|
|
10
|
+
`ctx.use(Script)(config)`; the built-in **`<Html.Scripts />`** component renders it.
|
|
11
|
+
It is the `Meta` / `<Html.Meta>` pair, but for scripts. The request CSP **nonce
|
|
12
|
+
is applied automatically to document-rendered scripts**; you never read or pass
|
|
13
|
+
it. (The one exception is an async script first encountered on a soft
|
|
14
|
+
navigation; see the nonce caveat under "Execution contract".)
|
|
15
|
+
|
|
16
|
+
Read "Execution contract" before choosing a shape: inline and ordered scripts
|
|
17
|
+
run only on a hard (document) load, never on a soft navigation.
|
|
16
18
|
|
|
17
19
|
## Setup
|
|
18
20
|
|
|
19
|
-
`<Scripts />` is a client component; place it in your Document (which is
|
|
21
|
+
`<Html.Scripts />` is a client component; place it in your Document (which is
|
|
20
22
|
`"use client"`). The default Document already includes both sites; a custom one
|
|
21
|
-
adds them next to `<
|
|
23
|
+
adds them next to `<Html.Meta />`:
|
|
22
24
|
|
|
23
25
|
```tsx
|
|
24
|
-
// document.tsx
|
|
25
|
-
|
|
26
|
+
// document.tsx
|
|
27
|
+
"use client";
|
|
28
|
+
import type { ReactNode } from "react";
|
|
29
|
+
import { Html } from "@rangojs/router/client";
|
|
26
30
|
|
|
27
|
-
export function Document({ children }) {
|
|
31
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
28
32
|
return (
|
|
29
33
|
<html lang="en" suppressHydrationWarning>
|
|
30
34
|
<head>
|
|
31
|
-
<
|
|
32
|
-
<Scripts /> {/* renders position: "head" scripts (the default) */}
|
|
35
|
+
<Html.Meta />
|
|
36
|
+
<Html.Scripts /> {/* renders position: "head" scripts (the default) */}
|
|
33
37
|
</head>
|
|
34
38
|
<body>
|
|
35
|
-
<Scripts position="body" />
|
|
39
|
+
<Html.Scripts position="body" />{" "}
|
|
40
|
+
{/* renders position: "body" scripts */}
|
|
36
41
|
{children}
|
|
37
42
|
</body>
|
|
38
43
|
</html>
|
|
@@ -60,7 +65,7 @@ ctx.use(Script)({
|
|
|
60
65
|
attributes: { "data-domain": "example.com" },
|
|
61
66
|
});
|
|
62
67
|
|
|
63
|
-
// 3. INLINE — `id` REQUIRED, raw JS body (escaped against </script> by <Scripts>).
|
|
68
|
+
// 3. INLINE — `id` REQUIRED, raw JS body (escaped against </script> by <Html.Scripts>).
|
|
64
69
|
// For GTM/GA4/Segment let the body self-inject its loader (see below).
|
|
65
70
|
ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
66
71
|
```
|
|
@@ -80,7 +85,8 @@ ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
|
80
85
|
- `type` — free string: `"module"`, `"application/ld+json"`, `"text/partytown"`, …
|
|
81
86
|
- `attributes` — React-cased (`crossOrigin`, not `crossorigin`) and React-typed
|
|
82
87
|
(`data-*`, `integrity`, `referrerPolicy`, …). Excluded: the fields the handle
|
|
83
|
-
manages (`id`/`src`/`async`/`defer`/`type`/`children`/`nonce
|
|
88
|
+
manages (`id`/`src`/`async`/`defer`/`type`/`children`/`nonce`/
|
|
89
|
+
`dangerouslySetInnerHTML`) and all `on*`
|
|
84
90
|
handlers (`onLoad`/`onError`/… — a config is serialized to the client, so a
|
|
85
91
|
function can't survive; use a `"use client"` component for callbacks).
|
|
86
92
|
|
|
@@ -95,7 +101,7 @@ innerHTML, which the HTML spec never executes). So:
|
|
|
95
101
|
| External ordered (`defer`/plain) | Yes | **No** — document-load only |
|
|
96
102
|
| External `async` | Yes | **Yes** — React loads the resource on first encounter |
|
|
97
103
|
|
|
98
|
-
`<Scripts>` enforces this
|
|
104
|
+
`<Html.Scripts>` enforces this: after hydration it **freezes** the inline +
|
|
99
105
|
ordered set to what was in the initial HTML, so a navigation never inserts an
|
|
100
106
|
inert (silently dead) `<script>`. Async configs stay reactive. Reusing an `id`
|
|
101
107
|
shapes the INITIAL document output (last-push-wins) — it does not re-run a script
|
|
@@ -133,9 +139,14 @@ the loader could run before the bootstrap. Instead let the bootstrap inject its
|
|
|
133
139
|
own loader (Google's snippet does exactly this):
|
|
134
140
|
|
|
135
141
|
```ts
|
|
136
|
-
|
|
142
|
+
// `initial` is pushed into dataLayer before the GTM start event, so tags that
|
|
143
|
+
// fire on the first page_view can read it (see per-route tagging below).
|
|
144
|
+
function gtmBootstrap(id: string, initial?: Record<string, string>): string {
|
|
137
145
|
return [
|
|
138
146
|
"window.dataLayer=window.dataLayer||[];",
|
|
147
|
+
// <Html.Scripts> escapes the body against </script> breakout, so plain
|
|
148
|
+
// JSON.stringify is enough here.
|
|
149
|
+
initial ? `window.dataLayer.push(${JSON.stringify(initial)});` : "",
|
|
139
150
|
'window.dataLayer.push({"gtm.start":new Date().getTime(),event:"gtm.js"});',
|
|
140
151
|
`(function(d,s,i){var j=d.createElement(s);j.async=true;j.src="https://www.googletagmanager.com/gtm.js?id="+encodeURIComponent(i);var f=d.getElementsByTagName(s)[0];f.parentNode.insertBefore(j,f);})(document,"script",${JSON.stringify(id)});`,
|
|
141
152
|
].join("");
|
|
@@ -157,7 +168,7 @@ ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
|
157
168
|
// a route: same id, with content_group baked in
|
|
158
169
|
ctx.use(Script)({
|
|
159
170
|
id: "gtm",
|
|
160
|
-
children:
|
|
171
|
+
children: gtmBootstrap("GTM-XXXX", { content_group: "blog" }),
|
|
161
172
|
});
|
|
162
173
|
```
|
|
163
174
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-actions
|
|
3
|
-
description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, revalidation. Use when handling a form submission on the server, calling a server function from a client component,
|
|
3
|
+
description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, post-action revalidation, client-cache control (keepClientCache), and the default CSRF origin check. Use when handling a form submission on the server, calling a server function from a client component, choosing what re-renders after a mutation, needing optimistic UI after a mutation, or asking how actions are protected from CSRF.
|
|
4
4
|
argument-hint: "[action]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -11,6 +11,10 @@ the client. They are React's RSC mutation primitive — Rango uses them as-is
|
|
|
11
11
|
with no framework wrapper. All standard React hooks (`useActionState`,
|
|
12
12
|
`useFormStatus`, `useOptimistic`, `useTransition`) work directly.
|
|
13
13
|
|
|
14
|
+
This skill covers defining actions, calling them (with and without JavaScript),
|
|
15
|
+
validation, redirects, errors, authorization, what re-renders after an action,
|
|
16
|
+
and how the client cache reacts.
|
|
17
|
+
|
|
14
18
|
## When to Use Actions vs Loaders
|
|
15
19
|
|
|
16
20
|
| Need | Use |
|
|
@@ -48,9 +52,15 @@ export const urlpatterns = urls(({ path, loader, revalidate }) => [
|
|
|
48
52
|
// The loader belongs to the route that consumes its data — nest it inside
|
|
49
53
|
// the owning path() so the segment owns its data dependency.
|
|
50
54
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
51
|
-
|
|
55
|
+
// The route and its loaders re-run after every action by default. Narrow
|
|
56
|
+
// both to cart actions; on navigation, undefined keeps the default.
|
|
57
|
+
revalidate((ctx) =>
|
|
58
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
59
|
+
),
|
|
52
60
|
loader(CartLoader, () => [
|
|
53
|
-
revalidate((ctx) =>
|
|
61
|
+
revalidate((ctx) =>
|
|
62
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
63
|
+
),
|
|
54
64
|
]),
|
|
55
65
|
]),
|
|
56
66
|
]);
|
|
@@ -99,11 +109,13 @@ import { ChangedTenant } from "./context";
|
|
|
99
109
|
|
|
100
110
|
export const urlpatterns = urls(({ path, revalidate }) => [
|
|
101
111
|
path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
|
|
112
|
+
// The route re-runs after every action by default. Narrow it: after an
|
|
113
|
+
// action, re-run only when a tenant action changed this tenant.
|
|
102
114
|
revalidate((ctx) => {
|
|
103
|
-
if (!ctx.isAction(
|
|
115
|
+
if (!ctx.isAction()) return undefined; // navigation: keep the default
|
|
104
116
|
return (
|
|
105
|
-
ctx.
|
|
106
|
-
|
|
117
|
+
ctx.isAction(TenantActions) &&
|
|
118
|
+
ctx.context.get(ChangedTenant) === ctx.context.params.tenantId
|
|
107
119
|
);
|
|
108
120
|
}),
|
|
109
121
|
]),
|
|
@@ -139,6 +151,14 @@ export async function removeFromCart(productId: string): Promise<void> {
|
|
|
139
151
|
const userId = cookies().get("user-id")?.value;
|
|
140
152
|
await db.cart.delete({ userId, productId });
|
|
141
153
|
}
|
|
154
|
+
|
|
155
|
+
export async function updateQuantity(
|
|
156
|
+
productId: string,
|
|
157
|
+
delta: number,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const userId = cookies().get("user-id")?.value;
|
|
160
|
+
await db.cart.adjust({ userId, productId, delta });
|
|
161
|
+
}
|
|
142
162
|
```
|
|
143
163
|
|
|
144
164
|
### Inline (`"use server"` inside a server component)
|
|
@@ -192,9 +212,9 @@ export function AddToCartForm({ productId }: { productId: string }) {
|
|
|
192
212
|
### 2. `useActionState` — preserve return value + pending state
|
|
193
213
|
|
|
194
214
|
Standard React 19 hook. The action receives `(prevState, formData)` and its
|
|
195
|
-
return value becomes the new `state`.
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
return value becomes the new `state`. To keep what the user typed after a
|
|
216
|
+
validation error, return the submitted values in the state and render them as
|
|
217
|
+
`defaultValue` (see below).
|
|
198
218
|
|
|
199
219
|
Define the state shape next to the action so the client and server share
|
|
200
220
|
one type:
|
|
@@ -248,10 +268,12 @@ export function ProfileForm({ initial }: { initial: { name: string } }) {
|
|
|
248
268
|
}
|
|
249
269
|
```
|
|
250
270
|
|
|
251
|
-
**Why
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
**Why echo values into `defaultValue`** — React 19 resets a form's
|
|
272
|
+
uncontrolled fields after its `action` completes, and a no-JS submission
|
|
273
|
+
re-renders the whole page. In both cases the only thing that survives is what
|
|
274
|
+
you render, so return the submitted values in `state.values` and use them as
|
|
275
|
+
`defaultValue`. (A controlled `value` without an `onChange` would make the
|
|
276
|
+
inputs read-only.)
|
|
255
277
|
|
|
256
278
|
### 3. `useOptimistic` — instant UI before the action settles
|
|
257
279
|
|
|
@@ -384,12 +406,25 @@ const parsed = SignupSchema.safeParse(ctx.body);
|
|
|
384
406
|
When an action mutates data, the matched route tree may need to partially
|
|
385
407
|
re-render so the UI updates. Rango runs the action, then evaluates
|
|
386
408
|
`revalidate()` on matched segments and loaders. Each path, layout, parallel,
|
|
387
|
-
|
|
409
|
+
or loader rule decides whether that piece re-renders/re-resolves. An open
|
|
410
|
+
intercept's handler re-runs on each action render (a prerendered intercept
|
|
411
|
+
variant reuses its stored output); its loaders carry their own rules
|
|
412
|
+
(`loader(Def, () => [revalidate(...)])` — an intercept-level `revalidate()`
|
|
413
|
+
throws at definition time).
|
|
388
414
|
|
|
389
415
|
Use `ctx.isAction()` for specific actions or modules. It accepts one action,
|
|
390
|
-
several actions, or a namespace import (`import * as CartActions`).
|
|
391
|
-
|
|
392
|
-
|
|
416
|
+
several actions, or a namespace import (`import * as CartActions`). Wrap it by
|
|
417
|
+
what the segment's default already does:
|
|
418
|
+
|
|
419
|
+
- **Add a signal** where the default skips (a parent layout or layout-level
|
|
420
|
+
parallel): `ctx.isAction(CartActions) || undefined` re-renders it after cart
|
|
421
|
+
actions and defers otherwise. On a loader or route segment it changes nothing.
|
|
422
|
+
- **Narrow after actions** where the default re-runs (loaders, the route, and
|
|
423
|
+
segments inside the `path()`):
|
|
424
|
+
`ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
|
|
425
|
+
actions and keeps the navigation default.
|
|
426
|
+
- **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
|
|
427
|
+
too, so a loader stops refetching when params change.
|
|
393
428
|
|
|
394
429
|
```typescript
|
|
395
430
|
// urls.tsx — inside the urls() callback. Nest each loader inside the path(),
|
|
@@ -404,18 +439,25 @@ urls(({ path, loader, revalidate }) => [
|
|
|
404
439
|
loader(StaticHomepageLoader, () => [revalidate(() => false)]),
|
|
405
440
|
]),
|
|
406
441
|
|
|
407
|
-
// Re-render the cart page handler AND re-resolve its loader after cart
|
|
442
|
+
// Re-render the cart page handler AND re-resolve its loader only after cart
|
|
443
|
+
// actions (both re-run after every action by default)
|
|
408
444
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
409
|
-
revalidate((ctx) =>
|
|
445
|
+
revalidate((ctx) =>
|
|
446
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
447
|
+
),
|
|
410
448
|
loader(CartLoader, () => [
|
|
411
|
-
revalidate((ctx) =>
|
|
449
|
+
revalidate((ctx) =>
|
|
450
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
451
|
+
),
|
|
412
452
|
]),
|
|
413
453
|
]),
|
|
414
454
|
|
|
415
|
-
// Re-run after
|
|
455
|
+
// Re-run the loader only after actions exported by the account actions module
|
|
416
456
|
path("/account", AccountPage, { name: "account" }, () => [
|
|
417
457
|
loader(AccountLoader, () => [
|
|
418
|
-
revalidate((ctx) =>
|
|
458
|
+
revalidate((ctx) =>
|
|
459
|
+
ctx.isAction() ? ctx.isAction(AccountActions) : undefined,
|
|
460
|
+
),
|
|
419
461
|
]),
|
|
420
462
|
]),
|
|
421
463
|
]);
|
|
@@ -424,7 +466,8 @@ urls(({ path, loader, revalidate }) => [
|
|
|
424
466
|
The raw `actionId` string stays available for broad path filters:
|
|
425
467
|
|
|
426
468
|
```typescript
|
|
427
|
-
//
|
|
469
|
+
// On a parent layout (skipped after actions by default): also re-render after
|
|
470
|
+
// any action under src/actions/account/, including modules not imported here.
|
|
428
471
|
revalidate(
|
|
429
472
|
({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
|
|
430
473
|
);
|
|
@@ -446,15 +489,19 @@ stale context. Share the same `revalidate` predicate on both producer and
|
|
|
446
489
|
consumer:
|
|
447
490
|
|
|
448
491
|
```typescript
|
|
492
|
+
import type { Revalidate } from "@rangojs/router";
|
|
449
493
|
import * as CartActions from "./actions/cart";
|
|
450
494
|
|
|
451
|
-
const revalidateCart = (ctx) =>
|
|
495
|
+
const revalidateCart: Revalidate = (ctx) =>
|
|
496
|
+
ctx.isAction(CartActions) || undefined;
|
|
452
497
|
|
|
453
498
|
urls(({ path, layout, loader, revalidate }) => [
|
|
454
499
|
layout(CartLayout, () => [
|
|
455
|
-
revalidate(revalidateCart), // producer
|
|
500
|
+
revalidate(revalidateCart), // producer: re-renders after cart actions (skipped by default)
|
|
456
501
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
457
|
-
|
|
502
|
+
// consumer: loaders re-run after every action anyway; the shared
|
|
503
|
+
// contract names the dependency
|
|
504
|
+
loader(CartItemsLoader, () => [revalidate(revalidateCart)]),
|
|
458
505
|
]),
|
|
459
506
|
]),
|
|
460
507
|
]);
|
|
@@ -462,6 +509,52 @@ urls(({ path, layout, loader, revalidate }) => [
|
|
|
462
509
|
|
|
463
510
|
See `/middleware` for the full cross-segment revalidation contract.
|
|
464
511
|
|
|
512
|
+
## Client Cache After an Action
|
|
513
|
+
|
|
514
|
+
`revalidate()` decides what the server re-renders **in the action's response**.
|
|
515
|
+
Separately, the browser invalidates its own caches once the action's response
|
|
516
|
+
arrives: it marks the history cache stale (Back renders the cached entry, then
|
|
517
|
+
revalidates), flushes the prefetch cache, rotates the router's state cookie,
|
|
518
|
+
and tells other open tabs to do the same. This happens for every action that
|
|
519
|
+
reached the server, including ones that threw. No setup is needed.
|
|
520
|
+
|
|
521
|
+
### `keepClientCache()` — skip it for no-op actions
|
|
522
|
+
|
|
523
|
+
Some hot actions change nothing a route renders: a debounced draft autosave, an
|
|
524
|
+
analytics event, a save that turned out to be a no-op. Call `keepClientCache()`
|
|
525
|
+
inside the action to tell the client to leave its caches alone for this call:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// app/actions/draft.ts
|
|
529
|
+
"use server";
|
|
530
|
+
|
|
531
|
+
import { keepClientCache } from "@rangojs/router";
|
|
532
|
+
|
|
533
|
+
export async function saveDraft(formData: FormData) {
|
|
534
|
+
const changed = await drafts.persist(formData);
|
|
535
|
+
if (!changed) keepClientCache(); // decided per call, not per action
|
|
536
|
+
return { saved: true };
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
What it suppresses, for this action only: the stale-marking of the history
|
|
541
|
+
cache, the prefetch flush, the state-cookie rotation, the cross-tab broadcast,
|
|
542
|
+
and the background re-fetch the client runs when the user navigated away while
|
|
543
|
+
the action was in flight. What it does not change:
|
|
544
|
+
|
|
545
|
+
- The action's own response still carries the revalidation render your
|
|
546
|
+
`revalidate()` rules select, and the client applies it.
|
|
547
|
+
- A concurrent action that did not call it still invalidates.
|
|
548
|
+
- An explicit `invalidateClientCache()` in the same action still rotates the
|
|
549
|
+
cookie, so invalidation wins.
|
|
550
|
+
|
|
551
|
+
It is a server directive (an internal response header the client reads).
|
|
552
|
+
Outside a request it is a no-op with a dev warning; the client-side import is
|
|
553
|
+
also a no-op with a dev warning. Like `cookies()`, it throws inside a
|
|
554
|
+
`cache()` / `"use cache"` boundary. For the opposite case — a mutation the
|
|
555
|
+
router cannot see, such as a REST call or WebSocket push — use
|
|
556
|
+
`invalidateClientCache()` (see `/hooks`, `state.md`).
|
|
557
|
+
|
|
465
558
|
## Redirects
|
|
466
559
|
|
|
467
560
|
`redirect()` works inside actions. Both `return redirect(...)` and
|
|
@@ -527,36 +620,55 @@ is usually too aggressive.
|
|
|
527
620
|
### Unexpected errors — let them throw
|
|
528
621
|
|
|
529
622
|
Throw for genuinely exceptional conditions (network failure, DB outage,
|
|
530
|
-
auth violation). The nearest `errorBoundary()`
|
|
531
|
-
them
|
|
623
|
+
auth violation). The nearest `errorBoundary()` above the page the action was
|
|
624
|
+
called from catches them, and the boundary renders inside route middleware like
|
|
625
|
+
a normal render.
|
|
532
626
|
|
|
533
627
|
```typescript
|
|
534
|
-
|
|
535
|
-
|
|
628
|
+
// urls.tsx — inside the urls() callback
|
|
536
629
|
layout(CheckoutLayout, () => [
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
<
|
|
630
|
+
// Server fallback: receives { error } only (ErrorInfo); there is no reset().
|
|
631
|
+
errorBoundary(({ error }) => (
|
|
632
|
+
<div role="alert">
|
|
633
|
+
<p>
|
|
634
|
+
{error.name === "ForbiddenError"
|
|
635
|
+
? "You cannot change this order."
|
|
636
|
+
: "Checkout failed."}
|
|
637
|
+
</p>
|
|
638
|
+
<a href="/checkout">Try again</a>
|
|
541
639
|
</div>
|
|
542
640
|
)),
|
|
543
641
|
path("/checkout", CheckoutPage, { name: "checkout" }),
|
|
544
642
|
]);
|
|
545
643
|
```
|
|
546
644
|
|
|
645
|
+
In production `error.message` is replaced with `"An error occurred"` (and
|
|
646
|
+
`stack` / `cause` are dropped), while `error.name` and `error.code` are kept —
|
|
647
|
+
branch on `name` or `code`, not on the message.
|
|
648
|
+
|
|
547
649
|
### Not found from an action
|
|
548
650
|
|
|
651
|
+
`notFound()` inside an action is handled like any other thrown error: it
|
|
652
|
+
renders the nearest `errorBoundary()` with `error.name === "DataNotFoundError"`.
|
|
653
|
+
`notFoundBoundary()` handles `notFound()` from handlers and loaders, not from
|
|
654
|
+
actions.
|
|
655
|
+
|
|
549
656
|
```typescript
|
|
657
|
+
// app/actions/posts.ts
|
|
658
|
+
"use server";
|
|
659
|
+
|
|
550
660
|
import { notFound } from "@rangojs/router";
|
|
551
661
|
|
|
552
662
|
export async function deletePost(id: string): Promise<void> {
|
|
553
|
-
"use server";
|
|
554
663
|
const post = await db.posts.find(id);
|
|
555
|
-
if (!post) notFound("Post not found"); //
|
|
664
|
+
if (!post) notFound("Post not found"); // → nearest errorBoundary()
|
|
556
665
|
await db.posts.delete(id);
|
|
557
666
|
}
|
|
558
667
|
```
|
|
559
668
|
|
|
669
|
+
For a recoverable "already gone" case, return a result the form can show
|
|
670
|
+
(`{ error: "Post not found" }`) instead of throwing.
|
|
671
|
+
|
|
560
672
|
### Authorization in actions
|
|
561
673
|
|
|
562
674
|
Route middleware does **not** wrap action execution — only global
|
|
@@ -564,11 +676,18 @@ middleware (`router.use()`) does. Auth checks must therefore live in
|
|
|
564
676
|
`router.use()` or inside the action itself. Don't rely on a route-level
|
|
565
677
|
`middleware()` to gate action access.
|
|
566
678
|
|
|
679
|
+
An action is identified by its id, not by a URL: the client posts it to the
|
|
680
|
+
current page's URL, but any URL that matches a route can carry it. A
|
|
681
|
+
pattern-scoped `router.use("/admin/*", ...)` therefore guards action requests
|
|
682
|
+
posted from `/admin/*` pages, not the action itself. Use global middleware to
|
|
683
|
+
establish identity (load the session), and authorize sensitive actions inside
|
|
684
|
+
the action body.
|
|
685
|
+
|
|
567
686
|
```typescript
|
|
568
|
-
// router.tsx — global
|
|
687
|
+
// router.tsx — global middleware wraps action + render
|
|
569
688
|
const router = createRouter()
|
|
570
|
-
.use(authInit)
|
|
571
|
-
.use("/admin/*", requireAdmin) //
|
|
689
|
+
.use(authInit) // loads the session for every request, actions included
|
|
690
|
+
.use("/admin/*", requireAdmin) // guards /admin pages and actions posted from them
|
|
572
691
|
.routes(urlpatterns);
|
|
573
692
|
```
|
|
574
693
|
|
|
@@ -590,7 +709,7 @@ export async function deleteOrder(orderId: string) {
|
|
|
590
709
|
if (!user) throw redirect("/login"); // unauthenticated → bounce to login
|
|
591
710
|
|
|
592
711
|
const order = await db.orders.get(orderId);
|
|
593
|
-
if (!order) notFound("Order not found"); // →
|
|
712
|
+
if (!order) notFound("Order not found"); // → errorBoundary (DataNotFoundError)
|
|
594
713
|
if (order.userId !== user.id) throw new ForbiddenError(); // → errorBoundary
|
|
595
714
|
|
|
596
715
|
await db.orders.delete(orderId);
|
|
@@ -601,10 +720,46 @@ export async function deleteOrder(orderId: string) {
|
|
|
601
720
|
> Responses thrown from actions are treated as errors and routed to the nearest
|
|
602
721
|
> `errorBoundary()`, not returned as real HTTP responses (the dev build warns
|
|
603
722
|
> when you do this). Use `redirect()` to send unauthenticated users to a login
|
|
604
|
-
> page, `notFound()`
|
|
605
|
-
>
|
|
606
|
-
>
|
|
607
|
-
|
|
723
|
+
> page, and `notFound()` or a domain error class (branch on `error.name` in the
|
|
724
|
+
> boundary) for missing or forbidden resources. For recoverable cases, return
|
|
725
|
+
> `{ error: "..." }` via `useActionState` instead of throwing.
|
|
726
|
+
|
|
727
|
+
## CSRF Protection
|
|
728
|
+
|
|
729
|
+
Actions are protected against cross-site request forgery by default, with no
|
|
730
|
+
token to thread through your forms. Before an action runs — called over JS or
|
|
731
|
+
posted by a no-JS form — the router's `originCheck` compares the request's
|
|
732
|
+
`Origin` header (or `Referer` when `Origin` is absent) with the request
|
|
733
|
+
protocol and `Host`:
|
|
734
|
+
|
|
735
|
+
| Request | Result |
|
|
736
|
+
| ---------------------------------------------- | ------------------------------------------------------------ |
|
|
737
|
+
| Same-origin `Origin` / `Referer` | Action runs |
|
|
738
|
+
| Cross-origin `Origin` / `Referer` | `403`, action never runs |
|
|
739
|
+
| `Origin: null` (sandboxed iframe, `data:` URL) | `403`, action never runs |
|
|
740
|
+
| Neither header (curl, server-to-server) | Action runs — a non-browser client carries no victim cookies |
|
|
741
|
+
| `Origin` present but no `Host` header | `403` (fails closed) |
|
|
742
|
+
|
|
743
|
+
The same gate covers fetchable-loader requests (`useFetchLoader()`,
|
|
744
|
+
`load({ method: "POST" })`). It runs before the action is decoded, so a
|
|
745
|
+
rejected request executes no action code. A rejection returns `403` with
|
|
746
|
+
`X-Rango-Origin-Check: failed` (in dev the body shows both header values),
|
|
747
|
+
reports to `onError` with phase `"origin"`, and emits a
|
|
748
|
+
`request.origin-rejected` telemetry event (`/observability`).
|
|
749
|
+
|
|
750
|
+
What the check does not do:
|
|
751
|
+
|
|
752
|
+
- **Authorize.** A same-origin request from a logged-out or unprivileged user
|
|
753
|
+
passes. Keep the checks from "Authorization in actions".
|
|
754
|
+
- **Run before global middleware.** `router.use()` middleware wraps the whole
|
|
755
|
+
request, so it runs for a cross-origin POST before the `403`. Don't mutate
|
|
756
|
+
state in middleware assuming the origin was already checked.
|
|
757
|
+
- **Cover response routes.** `path.json()` and the other response-route tags
|
|
758
|
+
are outside the gate; see `/response-routes` → "CSRF".
|
|
759
|
+
- **Trust proxies.** `X-Forwarded-Host` / `X-Forwarded-Proto` are ignored.
|
|
760
|
+
Behind a proxy that rewrites `Host`, or to allow an extra trusted origin, pass
|
|
761
|
+
an `originCheck` function (`/router-setup` → "Origin check").
|
|
762
|
+
`originCheck: false` disables the gate.
|
|
608
763
|
|
|
609
764
|
## Action Context
|
|
610
765
|
|
|
@@ -633,18 +788,15 @@ request scope.
|
|
|
633
788
|
|
|
634
789
|
### Constraints
|
|
635
790
|
|
|
636
|
-
| Constraint | Why
|
|
637
|
-
| -------------------------------------------------------- |
|
|
638
|
-
| Actions cannot return or throw a non-redirect `Response` | Return values go through RSC Flight serialization. A
|
|
639
|
-
| Route DSL `middleware()` does not wrap actions | Actions execute before route middleware. Only global `router.use()` middleware (and its scoped variants) wrap action execution.
|
|
640
|
-
|
|
|
791
|
+
| Constraint | Why |
|
|
792
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
793
|
+
| Actions cannot return or throw a non-redirect `Response` | Return values go through RSC Flight serialization. A returned non-redirect `Response` is discarded and the page re-renders; a thrown one is treated as a regular error and hits the nearest `errorBoundary()` (dev warns in both cases). Use `redirect()`, `notFound()`, or domain errors. |
|
|
794
|
+
| Route DSL `middleware()` does not wrap actions | Actions execute before route middleware. Only global `router.use()` middleware (and its scoped variants) wrap action execution. |
|
|
795
|
+
| Prefer actions over fetchable-loader POSTs for writes | Only actions trigger the post-action revalidation render and the client-cache invalidation. A `useFetchLoader()` / `load({ method: "POST" })` call does neither (see `/loader` → "Mutation Context"). |
|
|
641
796
|
|
|
642
797
|
Cookies/headers set in **global** `router.use()` middleware DO propagate to
|
|
643
|
-
action responses (the same merge path as a normal render).
|
|
644
|
-
|
|
645
|
-
middleware })` on a POST request) is that it cannot set cookies — set them
|
|
646
|
-
in the loader body instead. See `/middleware` for the full middleware
|
|
647
|
-
contract.
|
|
798
|
+
action responses (the same merge path as a normal render). See `/middleware`
|
|
799
|
+
for the full middleware contract.
|
|
648
800
|
|
|
649
801
|
## File Uploads
|
|
650
802
|
|
|
@@ -729,22 +881,30 @@ function AddButton({ productId }: { productId: string }) {
|
|
|
729
881
|
>
|
|
730
882
|
{state === "loading" ? "Adding…" : "Add"}
|
|
731
883
|
</button>
|
|
732
|
-
{error && <p role="alert">{error.message}</p>}
|
|
884
|
+
{error instanceof Error && <p role="alert">{error.message}</p>}
|
|
733
885
|
</>
|
|
734
886
|
);
|
|
735
887
|
}
|
|
736
888
|
```
|
|
737
889
|
|
|
890
|
+
`error` is typed `unknown` (whatever the action threw), so narrow it before
|
|
891
|
+
reading `.message`.
|
|
892
|
+
|
|
738
893
|
`useActionState` and `useAction` are complementary — use `useActionState`
|
|
739
894
|
for `<form action={...}>` flows, `useAction` for imperative button clicks
|
|
740
|
-
or to observe an action triggered elsewhere on the page.
|
|
895
|
+
or to observe an action triggered elsewhere on the page. See `/hooks`
|
|
896
|
+
(`handle-and-actions.md`) for the full `useAction` state shape and string
|
|
897
|
+
matching.
|
|
741
898
|
|
|
742
899
|
## Progressive Enhancement
|
|
743
900
|
|
|
744
901
|
`<form action={serverAction}>` works without JavaScript: the form posts as a
|
|
745
902
|
normal HTTP request, the action runs, and the page re-renders server-side.
|
|
746
|
-
For PE to work,
|
|
747
|
-
|
|
903
|
+
For PE to work, the form's `action` must be the server reference itself, or a
|
|
904
|
+
`.bind(null, ...)` of it (React encodes bound arguments into the form, as in
|
|
905
|
+
pattern 1 above), or the dispatcher `useActionState` returns for it. A
|
|
906
|
+
client-side closure that calls the action is not a server reference and breaks
|
|
907
|
+
PE (see "File Uploads"). The simplest PE-safe shape:
|
|
748
908
|
|
|
749
909
|
```tsx
|
|
750
910
|
// Works with no-JS submission
|
|
@@ -764,7 +924,8 @@ export async function submitName(formData: FormData) {
|
|
|
764
924
|
|
|
765
925
|
`useActionState` and `useOptimistic` only enhance the experience once JS is
|
|
766
926
|
loaded — without JS, the underlying action still runs and the page still
|
|
767
|
-
re-renders
|
|
927
|
+
re-renders (with the `useActionState` result restored as its state). Don't rely
|
|
928
|
+
on client-only state for required form behavior.
|
|
768
929
|
|
|
769
930
|
## Cross-references
|
|
770
931
|
|
|
@@ -34,9 +34,11 @@ batched.
|
|
|
34
34
|
1. **Handles record data at render time.** The handler pushes to a handle
|
|
35
35
|
(`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
|
|
36
36
|
the cache miss for `cache()`. (Loader bodies can push handles too — see
|
|
37
|
-
`/loader` — but a loader push is request-time and is NOT part of
|
|
38
|
-
replayed artifact; a manifest handle must be pushed by the code that
|
|
39
|
-
frozen with the shell.
|
|
37
|
+
`/loader` — but a live loader's push is request-time and is NOT part of
|
|
38
|
+
the replayed artifact; a manifest handle must be pushed by the code that
|
|
39
|
+
gets frozen with the shell. An `ssr: false` loader on a `ppr` route is the
|
|
40
|
+
exception: its settled pushes are recorded and replayed, and its re-run
|
|
41
|
+
replaces them with its live pushes — `/ppr` "On a shell HIT".)
|
|
40
42
|
2. **Replay on every hit.** Handle data is stored with the Flight payload
|
|
41
43
|
and replayed into the handle store on cache/prerender hits — handler code
|
|
42
44
|
does not re-run, but its pushes do.
|
|
@@ -94,7 +96,7 @@ import { RenderedProducts } from "../handles/rendered-products";
|
|
|
94
96
|
export const PriceLoader = createLoader(async (ctx) => {
|
|
95
97
|
await ctx.rendered();
|
|
96
98
|
const ids = ctx.get(RenderedProducts);
|
|
97
|
-
return db.pricesFor(ids); //
|
|
99
|
+
return db.pricesFor(ids); // Record<string, number> keyed by product id
|
|
98
100
|
});
|
|
99
101
|
```
|
|
100
102
|
|
|
@@ -138,9 +140,11 @@ cache({ ttl: 600, tags: ["products"] }, () => [
|
|
|
138
140
|
## Contract and gotchas
|
|
139
141
|
|
|
140
142
|
- **The manifest is exactly as fresh as the shell.** Replayed handle data is
|
|
141
|
-
frozen with the payload. To change _which_ products render,
|
|
142
|
-
shell (`updateTag("products")
|
|
143
|
-
|
|
143
|
+
frozen with the payload. To change _which_ products render, refresh the
|
|
144
|
+
shell — for `cache()`, `updateTag("products")` or TTL expiry; for
|
|
145
|
+
`Prerender`, a rebuild (tag invalidation does not reach build-time payloads,
|
|
146
|
+
see `/prerender`). Never treat the loader as the refresh path for the list
|
|
147
|
+
itself. This is the point:
|
|
144
148
|
shell and holes cannot desync because they share one artifact.
|
|
145
149
|
- **No request-scoped data in a manifest handle.** The handle data is baked
|
|
146
150
|
into a shared artifact — the same cross-user rule as any cached content.
|
|
@@ -187,6 +191,7 @@ production), like every cache-path behavior.
|
|
|
187
191
|
|
|
188
192
|
- `/prerender` — `Prerender`/`Passthrough`, build flow, passthrough fallback
|
|
189
193
|
- `/caching` — segment `cache()`, stores, tags
|
|
194
|
+
- `/ppr` — cached HTML shells; its holes are the same live loaders
|
|
190
195
|
- `/loader` — loader context, `ctx.rendered()`, streaming
|
|
191
196
|
- `/hooks` — `useHandle` for reading handle data in client components
|
|
192
197
|
- `/rango` → "Passing data down the tree" — this pattern is the frozen→live
|