@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.
- package/README.md +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -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-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- 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/prerender/build-shell-capture.d.ts +11 -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/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- 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 +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- 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/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.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-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- 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 +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- 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/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -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 +75 -57
- 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 +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -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 +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- 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 +26 -6
- 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/serve-shell-request.md +188 -0
- 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 +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- 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 +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- 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 +6 -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 +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- 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 +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- 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 +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -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 +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -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 +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
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
|
|
@@ -108,7 +114,10 @@ during navigation.
|
|
|
108
114
|
> means it is silently dropped. If a loader must contribute an inline script
|
|
109
115
|
> to the document, register it `loader(Def, { ssr: false })` so the
|
|
110
116
|
> document render awaits the push; otherwise push from a handler (or use an
|
|
111
|
-
> `async` config, which stays reactive).
|
|
117
|
+
> `async` config, which stays reactive). On a `ppr` route a loader without
|
|
118
|
+
> `ssr: false` never runs during the shell capture, so its inline/ordered
|
|
119
|
+
> script is dropped on every shell HIT; with `ssr: false` the push bakes into
|
|
120
|
+
> the shell.
|
|
112
121
|
|
|
113
122
|
**Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
|
|
114
123
|
holds for DOCUMENT-RENDERED scripts (they carry the nonce in the SSR HTML). An
|
|
@@ -133,9 +142,14 @@ the loader could run before the bootstrap. Instead let the bootstrap inject its
|
|
|
133
142
|
own loader (Google's snippet does exactly this):
|
|
134
143
|
|
|
135
144
|
```ts
|
|
136
|
-
|
|
145
|
+
// `initial` is pushed into dataLayer before the GTM start event, so tags that
|
|
146
|
+
// fire on the first page_view can read it (see per-route tagging below).
|
|
147
|
+
function gtmBootstrap(id: string, initial?: Record<string, string>): string {
|
|
137
148
|
return [
|
|
138
149
|
"window.dataLayer=window.dataLayer||[];",
|
|
150
|
+
// <Html.Scripts> escapes the body against </script> breakout, so plain
|
|
151
|
+
// JSON.stringify is enough here.
|
|
152
|
+
initial ? `window.dataLayer.push(${JSON.stringify(initial)});` : "",
|
|
139
153
|
'window.dataLayer.push({"gtm.start":new Date().getTime(),event:"gtm.js"});',
|
|
140
154
|
`(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
155
|
].join("");
|
|
@@ -157,7 +171,7 @@ ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
|
157
171
|
// a route: same id, with content_group baked in
|
|
158
172
|
ctx.use(Script)({
|
|
159
173
|
id: "gtm",
|
|
160
|
-
children:
|
|
174
|
+
children: gtmBootstrap("GTM-XXXX", { content_group: "blog" }),
|
|
161
175
|
});
|
|
162
176
|
```
|
|
163
177
|
|
|
@@ -174,6 +188,10 @@ Otherwise allow the vendor hosts. For GTM/GA4 (Google's wildcards): `script-src
|
|
|
174
188
|
`frame-src https://*.googletagmanager.com` for the GTM `<noscript>` iframe. See
|
|
175
189
|
[Google's CSP guide](https://developers.google.com/tag-platform/security/guides/csp).
|
|
176
190
|
|
|
191
|
+
A per-request nonce also takes a `ppr` route off its shell: a shared shell
|
|
192
|
+
cannot carry one request's nonce, so the route renders normally (no shell) and
|
|
193
|
+
warns once per key. See `/ppr` → "What always renders without a shell".
|
|
194
|
+
|
|
177
195
|
## Not covered (do it yourself)
|
|
178
196
|
|
|
179
197
|
- **`onLoad` / `onReady` / `onError`** — callbacks can't cross the server handle
|
|
@@ -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 |
|
|
@@ -33,6 +37,17 @@ Actions mutate state; route handlers and loaders read the latest state. After
|
|
|
33
37
|
an action finishes, Rango performs a server-side revalidation render for the
|
|
34
38
|
matched route so the UI receives fresh segment output and loader data.
|
|
35
39
|
|
|
40
|
+
Segments under a route `cache()` re-render fresh on that render (actions skip
|
|
41
|
+
the route cache lookup and write). A loader with its own `cache()` and a
|
|
42
|
+
`"use cache"` read return their stored entry, and a `ppr` route's stored shell
|
|
43
|
+
is not evicted, unless the action invalidates their tags.
|
|
44
|
+
`await updateTag(...tags)` purges them before the revalidation render, so the
|
|
45
|
+
action's own response reads fresh (`revalidateTag()` gives that render fresh
|
|
46
|
+
reads too; only its durable write runs in the background). On a store that keeps
|
|
47
|
+
per-isolate memos (`CFCacheStore`, `VercelCacheStore`) the response also sets
|
|
48
|
+
the fresh-reads cookie (`rango-state-fresh` by default), so the same user's next
|
|
49
|
+
requests skip those memos (see `/caching` → "The fresh-reads cookie").
|
|
50
|
+
|
|
36
51
|
The main control point is `revalidate((ctx) => ...)` on the segment that owns
|
|
37
52
|
the data. Match specific actions by imported reference with `ctx.isAction()`;
|
|
38
53
|
use raw `actionId` only when you intentionally need path or directory matching.
|
|
@@ -48,9 +63,15 @@ export const urlpatterns = urls(({ path, loader, revalidate }) => [
|
|
|
48
63
|
// The loader belongs to the route that consumes its data — nest it inside
|
|
49
64
|
// the owning path() so the segment owns its data dependency.
|
|
50
65
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
51
|
-
|
|
66
|
+
// The route and its loaders re-run after every action by default. Narrow
|
|
67
|
+
// both to cart actions; on navigation, undefined keeps the default.
|
|
68
|
+
revalidate((ctx) =>
|
|
69
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
70
|
+
),
|
|
52
71
|
loader(CartLoader, () => [
|
|
53
|
-
revalidate((ctx) =>
|
|
72
|
+
revalidate((ctx) =>
|
|
73
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
74
|
+
),
|
|
54
75
|
]),
|
|
55
76
|
]),
|
|
56
77
|
]);
|
|
@@ -99,11 +120,13 @@ import { ChangedTenant } from "./context";
|
|
|
99
120
|
|
|
100
121
|
export const urlpatterns = urls(({ path, revalidate }) => [
|
|
101
122
|
path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
|
|
123
|
+
// The route re-runs after every action by default. Narrow it: after an
|
|
124
|
+
// action, re-run only when a tenant action changed this tenant.
|
|
102
125
|
revalidate((ctx) => {
|
|
103
|
-
if (!ctx.isAction(
|
|
126
|
+
if (!ctx.isAction()) return undefined; // navigation: keep the default
|
|
104
127
|
return (
|
|
105
|
-
ctx.
|
|
106
|
-
|
|
128
|
+
ctx.isAction(TenantActions) &&
|
|
129
|
+
ctx.context.get(ChangedTenant) === ctx.context.params.tenantId
|
|
107
130
|
);
|
|
108
131
|
}),
|
|
109
132
|
]),
|
|
@@ -139,6 +162,14 @@ export async function removeFromCart(productId: string): Promise<void> {
|
|
|
139
162
|
const userId = cookies().get("user-id")?.value;
|
|
140
163
|
await db.cart.delete({ userId, productId });
|
|
141
164
|
}
|
|
165
|
+
|
|
166
|
+
export async function updateQuantity(
|
|
167
|
+
productId: string,
|
|
168
|
+
delta: number,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const userId = cookies().get("user-id")?.value;
|
|
171
|
+
await db.cart.adjust({ userId, productId, delta });
|
|
172
|
+
}
|
|
142
173
|
```
|
|
143
174
|
|
|
144
175
|
### Inline (`"use server"` inside a server component)
|
|
@@ -192,9 +223,9 @@ export function AddToCartForm({ productId }: { productId: string }) {
|
|
|
192
223
|
### 2. `useActionState` — preserve return value + pending state
|
|
193
224
|
|
|
194
225
|
Standard React 19 hook. The action receives `(prevState, formData)` and its
|
|
195
|
-
return value becomes the new `state`.
|
|
196
|
-
|
|
197
|
-
|
|
226
|
+
return value becomes the new `state`. To keep what the user typed after a
|
|
227
|
+
validation error, return the submitted values in the state and render them as
|
|
228
|
+
`defaultValue` (see below).
|
|
198
229
|
|
|
199
230
|
Define the state shape next to the action so the client and server share
|
|
200
231
|
one type:
|
|
@@ -248,10 +279,12 @@ export function ProfileForm({ initial }: { initial: { name: string } }) {
|
|
|
248
279
|
}
|
|
249
280
|
```
|
|
250
281
|
|
|
251
|
-
**Why
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
**Why echo values into `defaultValue`** — React 19 resets a form's
|
|
283
|
+
uncontrolled fields after its `action` completes, and a no-JS submission
|
|
284
|
+
re-renders the whole page. In both cases the only thing that survives is what
|
|
285
|
+
you render, so return the submitted values in `state.values` and use them as
|
|
286
|
+
`defaultValue`. (A controlled `value` without an `onChange` would make the
|
|
287
|
+
inputs read-only.)
|
|
255
288
|
|
|
256
289
|
### 3. `useOptimistic` — instant UI before the action settles
|
|
257
290
|
|
|
@@ -384,12 +417,25 @@ const parsed = SignupSchema.safeParse(ctx.body);
|
|
|
384
417
|
When an action mutates data, the matched route tree may need to partially
|
|
385
418
|
re-render so the UI updates. Rango runs the action, then evaluates
|
|
386
419
|
`revalidate()` on matched segments and loaders. Each path, layout, parallel,
|
|
387
|
-
|
|
420
|
+
or loader rule decides whether that piece re-renders/re-resolves. An open
|
|
421
|
+
intercept's handler re-runs on each action render (a prerendered intercept
|
|
422
|
+
variant reuses its stored output); its loaders carry their own rules
|
|
423
|
+
(`loader(Def, () => [revalidate(...)])` — an intercept-level `revalidate()`
|
|
424
|
+
throws at definition time).
|
|
388
425
|
|
|
389
426
|
Use `ctx.isAction()` for specific actions or modules. It accepts one action,
|
|
390
|
-
several actions, or a namespace import (`import * as CartActions`).
|
|
391
|
-
|
|
392
|
-
|
|
427
|
+
several actions, or a namespace import (`import * as CartActions`). Wrap it by
|
|
428
|
+
what the segment's default already does:
|
|
429
|
+
|
|
430
|
+
- **Add a signal** where the default skips (a parent layout or layout-level
|
|
431
|
+
parallel): `ctx.isAction(CartActions) || undefined` re-renders it after cart
|
|
432
|
+
actions and defers otherwise. On a loader or route segment it changes nothing.
|
|
433
|
+
- **Narrow after actions** where the default re-runs (loaders, the route, and
|
|
434
|
+
segments inside the `path()`):
|
|
435
|
+
`ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
|
|
436
|
+
actions and keeps the navigation default.
|
|
437
|
+
- **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
|
|
438
|
+
too, so a loader stops refetching when params change.
|
|
393
439
|
|
|
394
440
|
```typescript
|
|
395
441
|
// urls.tsx — inside the urls() callback. Nest each loader inside the path(),
|
|
@@ -404,18 +450,25 @@ urls(({ path, loader, revalidate }) => [
|
|
|
404
450
|
loader(StaticHomepageLoader, () => [revalidate(() => false)]),
|
|
405
451
|
]),
|
|
406
452
|
|
|
407
|
-
// Re-render the cart page handler AND re-resolve its loader after cart
|
|
453
|
+
// Re-render the cart page handler AND re-resolve its loader only after cart
|
|
454
|
+
// actions (both re-run after every action by default)
|
|
408
455
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
409
|
-
revalidate((ctx) =>
|
|
456
|
+
revalidate((ctx) =>
|
|
457
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
458
|
+
),
|
|
410
459
|
loader(CartLoader, () => [
|
|
411
|
-
revalidate((ctx) =>
|
|
460
|
+
revalidate((ctx) =>
|
|
461
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
462
|
+
),
|
|
412
463
|
]),
|
|
413
464
|
]),
|
|
414
465
|
|
|
415
|
-
// Re-run after
|
|
466
|
+
// Re-run the loader only after actions exported by the account actions module
|
|
416
467
|
path("/account", AccountPage, { name: "account" }, () => [
|
|
417
468
|
loader(AccountLoader, () => [
|
|
418
|
-
revalidate((ctx) =>
|
|
469
|
+
revalidate((ctx) =>
|
|
470
|
+
ctx.isAction() ? ctx.isAction(AccountActions) : undefined,
|
|
471
|
+
),
|
|
419
472
|
]),
|
|
420
473
|
]),
|
|
421
474
|
]);
|
|
@@ -424,7 +477,8 @@ urls(({ path, loader, revalidate }) => [
|
|
|
424
477
|
The raw `actionId` string stays available for broad path filters:
|
|
425
478
|
|
|
426
479
|
```typescript
|
|
427
|
-
//
|
|
480
|
+
// On a parent layout (skipped after actions by default): also re-render after
|
|
481
|
+
// any action under src/actions/account/, including modules not imported here.
|
|
428
482
|
revalidate(
|
|
429
483
|
({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
|
|
430
484
|
);
|
|
@@ -446,15 +500,19 @@ stale context. Share the same `revalidate` predicate on both producer and
|
|
|
446
500
|
consumer:
|
|
447
501
|
|
|
448
502
|
```typescript
|
|
503
|
+
import type { Revalidate } from "@rangojs/router";
|
|
449
504
|
import * as CartActions from "./actions/cart";
|
|
450
505
|
|
|
451
|
-
const revalidateCart = (ctx) =>
|
|
506
|
+
const revalidateCart: Revalidate = (ctx) =>
|
|
507
|
+
ctx.isAction(CartActions) || undefined;
|
|
452
508
|
|
|
453
509
|
urls(({ path, layout, loader, revalidate }) => [
|
|
454
510
|
layout(CartLayout, () => [
|
|
455
|
-
revalidate(revalidateCart), // producer
|
|
511
|
+
revalidate(revalidateCart), // producer: re-renders after cart actions (skipped by default)
|
|
456
512
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
457
|
-
|
|
513
|
+
// consumer: loaders re-run after every action anyway; the shared
|
|
514
|
+
// contract names the dependency
|
|
515
|
+
loader(CartItemsLoader, () => [revalidate(revalidateCart)]),
|
|
458
516
|
]),
|
|
459
517
|
]),
|
|
460
518
|
]);
|
|
@@ -462,6 +520,52 @@ urls(({ path, layout, loader, revalidate }) => [
|
|
|
462
520
|
|
|
463
521
|
See `/middleware` for the full cross-segment revalidation contract.
|
|
464
522
|
|
|
523
|
+
## Client Cache After an Action
|
|
524
|
+
|
|
525
|
+
`revalidate()` decides what the server re-renders **in the action's response**.
|
|
526
|
+
Separately, the browser invalidates its own caches once the action's response
|
|
527
|
+
arrives: it marks the history cache stale (Back renders the cached entry, then
|
|
528
|
+
revalidates), flushes the prefetch cache, rotates the router's state cookie,
|
|
529
|
+
and tells other open tabs to do the same. This happens for every action that
|
|
530
|
+
reached the server, including ones that threw. No setup is needed.
|
|
531
|
+
|
|
532
|
+
### `keepClientCache()` — skip it for no-op actions
|
|
533
|
+
|
|
534
|
+
Some hot actions change nothing a route renders: a debounced draft autosave, an
|
|
535
|
+
analytics event, a save that turned out to be a no-op. Call `keepClientCache()`
|
|
536
|
+
inside the action to tell the client to leave its caches alone for this call:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// app/actions/draft.ts
|
|
540
|
+
"use server";
|
|
541
|
+
|
|
542
|
+
import { keepClientCache } from "@rangojs/router";
|
|
543
|
+
|
|
544
|
+
export async function saveDraft(formData: FormData) {
|
|
545
|
+
const changed = await drafts.persist(formData);
|
|
546
|
+
if (!changed) keepClientCache(); // decided per call, not per action
|
|
547
|
+
return { saved: true };
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
What it suppresses, for this action only: the stale-marking of the history
|
|
552
|
+
cache, the prefetch flush, the state-cookie rotation, the cross-tab broadcast,
|
|
553
|
+
and the background re-fetch the client runs when the user navigated away while
|
|
554
|
+
the action was in flight. What it does not change:
|
|
555
|
+
|
|
556
|
+
- The action's own response still carries the revalidation render your
|
|
557
|
+
`revalidate()` rules select, and the client applies it.
|
|
558
|
+
- A concurrent action that did not call it still invalidates.
|
|
559
|
+
- An explicit `invalidateClientCache()` in the same action still rotates the
|
|
560
|
+
cookie, so invalidation wins.
|
|
561
|
+
|
|
562
|
+
It is a server directive (an internal response header the client reads).
|
|
563
|
+
Outside a request it is a no-op with a dev warning; the client-side import is
|
|
564
|
+
also a no-op with a dev warning. Like `cookies()`, it throws inside a
|
|
565
|
+
`cache()` / `"use cache"` boundary. For the opposite case — a mutation the
|
|
566
|
+
router cannot see, such as a REST call or WebSocket push — use
|
|
567
|
+
`invalidateClientCache()` (see `/hooks`, `state.md`).
|
|
568
|
+
|
|
465
569
|
## Redirects
|
|
466
570
|
|
|
467
571
|
`redirect()` works inside actions. Both `return redirect(...)` and
|
|
@@ -527,36 +631,55 @@ is usually too aggressive.
|
|
|
527
631
|
### Unexpected errors — let them throw
|
|
528
632
|
|
|
529
633
|
Throw for genuinely exceptional conditions (network failure, DB outage,
|
|
530
|
-
auth violation). The nearest `errorBoundary()`
|
|
531
|
-
them
|
|
634
|
+
auth violation). The nearest `errorBoundary()` above the page the action was
|
|
635
|
+
called from catches them, and the boundary renders inside route middleware like
|
|
636
|
+
a normal render.
|
|
532
637
|
|
|
533
638
|
```typescript
|
|
534
|
-
|
|
535
|
-
|
|
639
|
+
// urls.tsx — inside the urls() callback
|
|
536
640
|
layout(CheckoutLayout, () => [
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
<
|
|
641
|
+
// Server fallback: receives { error } only (ErrorInfo); there is no reset().
|
|
642
|
+
errorBoundary(({ error }) => (
|
|
643
|
+
<div role="alert">
|
|
644
|
+
<p>
|
|
645
|
+
{error.name === "ForbiddenError"
|
|
646
|
+
? "You cannot change this order."
|
|
647
|
+
: "Checkout failed."}
|
|
648
|
+
</p>
|
|
649
|
+
<a href="/checkout">Try again</a>
|
|
541
650
|
</div>
|
|
542
651
|
)),
|
|
543
652
|
path("/checkout", CheckoutPage, { name: "checkout" }),
|
|
544
653
|
]);
|
|
545
654
|
```
|
|
546
655
|
|
|
656
|
+
In production `error.message` is replaced with `"An error occurred"` (and
|
|
657
|
+
`stack` / `cause` are dropped), while `error.name` and `error.code` are kept —
|
|
658
|
+
branch on `name` or `code`, not on the message.
|
|
659
|
+
|
|
547
660
|
### Not found from an action
|
|
548
661
|
|
|
662
|
+
`notFound()` inside an action is handled like any other thrown error: it
|
|
663
|
+
renders the nearest `errorBoundary()` with `error.name === "DataNotFoundError"`.
|
|
664
|
+
`notFoundBoundary()` handles `notFound()` from handlers and loaders, not from
|
|
665
|
+
actions.
|
|
666
|
+
|
|
549
667
|
```typescript
|
|
668
|
+
// app/actions/posts.ts
|
|
669
|
+
"use server";
|
|
670
|
+
|
|
550
671
|
import { notFound } from "@rangojs/router";
|
|
551
672
|
|
|
552
673
|
export async function deletePost(id: string): Promise<void> {
|
|
553
|
-
"use server";
|
|
554
674
|
const post = await db.posts.find(id);
|
|
555
|
-
if (!post) notFound("Post not found"); //
|
|
675
|
+
if (!post) notFound("Post not found"); // → nearest errorBoundary()
|
|
556
676
|
await db.posts.delete(id);
|
|
557
677
|
}
|
|
558
678
|
```
|
|
559
679
|
|
|
680
|
+
For a recoverable "already gone" case, return a result the form can show
|
|
681
|
+
(`{ error: "Post not found" }`) instead of throwing.
|
|
682
|
+
|
|
560
683
|
### Authorization in actions
|
|
561
684
|
|
|
562
685
|
Route middleware does **not** wrap action execution — only global
|
|
@@ -564,11 +687,18 @@ middleware (`router.use()`) does. Auth checks must therefore live in
|
|
|
564
687
|
`router.use()` or inside the action itself. Don't rely on a route-level
|
|
565
688
|
`middleware()` to gate action access.
|
|
566
689
|
|
|
690
|
+
An action is identified by its id, not by a URL: the client posts it to the
|
|
691
|
+
current page's URL, but any URL that matches a route can carry it. A
|
|
692
|
+
pattern-scoped `router.use("/admin/*", ...)` therefore guards action requests
|
|
693
|
+
posted from `/admin/*` pages, not the action itself. Use global middleware to
|
|
694
|
+
establish identity (load the session), and authorize sensitive actions inside
|
|
695
|
+
the action body.
|
|
696
|
+
|
|
567
697
|
```typescript
|
|
568
|
-
// router.tsx — global
|
|
698
|
+
// router.tsx — global middleware wraps action + render
|
|
569
699
|
const router = createRouter()
|
|
570
|
-
.use(authInit)
|
|
571
|
-
.use("/admin/*", requireAdmin) //
|
|
700
|
+
.use(authInit) // loads the session for every request, actions included
|
|
701
|
+
.use("/admin/*", requireAdmin) // guards /admin pages and actions posted from them
|
|
572
702
|
.routes(urlpatterns);
|
|
573
703
|
```
|
|
574
704
|
|
|
@@ -590,7 +720,7 @@ export async function deleteOrder(orderId: string) {
|
|
|
590
720
|
if (!user) throw redirect("/login"); // unauthenticated → bounce to login
|
|
591
721
|
|
|
592
722
|
const order = await db.orders.get(orderId);
|
|
593
|
-
if (!order) notFound("Order not found"); // →
|
|
723
|
+
if (!order) notFound("Order not found"); // → errorBoundary (DataNotFoundError)
|
|
594
724
|
if (order.userId !== user.id) throw new ForbiddenError(); // → errorBoundary
|
|
595
725
|
|
|
596
726
|
await db.orders.delete(orderId);
|
|
@@ -601,10 +731,46 @@ export async function deleteOrder(orderId: string) {
|
|
|
601
731
|
> Responses thrown from actions are treated as errors and routed to the nearest
|
|
602
732
|
> `errorBoundary()`, not returned as real HTTP responses (the dev build warns
|
|
603
733
|
> when you do this). Use `redirect()` to send unauthenticated users to a login
|
|
604
|
-
> page, `notFound()`
|
|
605
|
-
>
|
|
606
|
-
>
|
|
607
|
-
|
|
734
|
+
> page, and `notFound()` or a domain error class (branch on `error.name` in the
|
|
735
|
+
> boundary) for missing or forbidden resources. For recoverable cases, return
|
|
736
|
+
> `{ error: "..." }` via `useActionState` instead of throwing.
|
|
737
|
+
|
|
738
|
+
## CSRF Protection
|
|
739
|
+
|
|
740
|
+
Actions are protected against cross-site request forgery by default, with no
|
|
741
|
+
token to thread through your forms. Before an action runs — called over JS or
|
|
742
|
+
posted by a no-JS form — the router's `originCheck` compares the request's
|
|
743
|
+
`Origin` header (or `Referer` when `Origin` is absent) with the request
|
|
744
|
+
protocol and `Host`:
|
|
745
|
+
|
|
746
|
+
| Request | Result |
|
|
747
|
+
| ---------------------------------------------- | ------------------------------------------------------------ |
|
|
748
|
+
| Same-origin `Origin` / `Referer` | Action runs |
|
|
749
|
+
| Cross-origin `Origin` / `Referer` | `403`, action never runs |
|
|
750
|
+
| `Origin: null` (sandboxed iframe, `data:` URL) | `403`, action never runs |
|
|
751
|
+
| Neither header (curl, server-to-server) | Action runs — a non-browser client carries no victim cookies |
|
|
752
|
+
| `Origin` present but no `Host` header | `403` (fails closed) |
|
|
753
|
+
|
|
754
|
+
The same gate covers fetchable-loader requests (`useFetchLoader()`,
|
|
755
|
+
`load({ method: "POST" })`). It runs before the action is decoded, so a
|
|
756
|
+
rejected request executes no action code. A rejection returns `403` with
|
|
757
|
+
`X-Rango-Origin-Check: failed` (in dev the body shows both header values),
|
|
758
|
+
reports to `onError` with phase `"origin"`, and emits a
|
|
759
|
+
`request.origin-rejected` telemetry event (`/observability`).
|
|
760
|
+
|
|
761
|
+
What the check does not do:
|
|
762
|
+
|
|
763
|
+
- **Authorize.** A same-origin request from a logged-out or unprivileged user
|
|
764
|
+
passes. Keep the checks from "Authorization in actions".
|
|
765
|
+
- **Run before global middleware.** `router.use()` middleware wraps the whole
|
|
766
|
+
request, so it runs for a cross-origin POST before the `403`. Don't mutate
|
|
767
|
+
state in middleware assuming the origin was already checked.
|
|
768
|
+
- **Cover response routes.** `path.json()` and the other response-route tags
|
|
769
|
+
are outside the gate; see `/response-routes` → "CSRF".
|
|
770
|
+
- **Trust proxies.** `X-Forwarded-Host` / `X-Forwarded-Proto` are ignored.
|
|
771
|
+
Behind a proxy that rewrites `Host`, or to allow an extra trusted origin, pass
|
|
772
|
+
an `originCheck` function (`/router-setup` → "Origin check").
|
|
773
|
+
`originCheck: false` disables the gate.
|
|
608
774
|
|
|
609
775
|
## Action Context
|
|
610
776
|
|
|
@@ -633,18 +799,15 @@ request scope.
|
|
|
633
799
|
|
|
634
800
|
### Constraints
|
|
635
801
|
|
|
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
|
-
|
|
|
802
|
+
| Constraint | Why |
|
|
803
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
804
|
+
| 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. |
|
|
805
|
+
| Route DSL `middleware()` does not wrap actions | Actions execute before route middleware. Only global `router.use()` middleware (and its scoped variants) wrap action execution. |
|
|
806
|
+
| 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
807
|
|
|
642
808
|
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.
|
|
809
|
+
action responses (the same merge path as a normal render). See `/middleware`
|
|
810
|
+
for the full middleware contract.
|
|
648
811
|
|
|
649
812
|
## File Uploads
|
|
650
813
|
|
|
@@ -729,22 +892,30 @@ function AddButton({ productId }: { productId: string }) {
|
|
|
729
892
|
>
|
|
730
893
|
{state === "loading" ? "Adding…" : "Add"}
|
|
731
894
|
</button>
|
|
732
|
-
{error && <p role="alert">{error.message}</p>}
|
|
895
|
+
{error instanceof Error && <p role="alert">{error.message}</p>}
|
|
733
896
|
</>
|
|
734
897
|
);
|
|
735
898
|
}
|
|
736
899
|
```
|
|
737
900
|
|
|
901
|
+
`error` is typed `unknown` (whatever the action threw), so narrow it before
|
|
902
|
+
reading `.message`.
|
|
903
|
+
|
|
738
904
|
`useActionState` and `useAction` are complementary — use `useActionState`
|
|
739
905
|
for `<form action={...}>` flows, `useAction` for imperative button clicks
|
|
740
|
-
or to observe an action triggered elsewhere on the page.
|
|
906
|
+
or to observe an action triggered elsewhere on the page. See `/hooks`
|
|
907
|
+
(`handle-and-actions.md`) for the full `useAction` state shape and string
|
|
908
|
+
matching.
|
|
741
909
|
|
|
742
910
|
## Progressive Enhancement
|
|
743
911
|
|
|
744
912
|
`<form action={serverAction}>` works without JavaScript: the form posts as a
|
|
745
913
|
normal HTTP request, the action runs, and the page re-renders server-side.
|
|
746
|
-
For PE to work,
|
|
747
|
-
|
|
914
|
+
For PE to work, the form's `action` must be the server reference itself, or a
|
|
915
|
+
`.bind(null, ...)` of it (React encodes bound arguments into the form, as in
|
|
916
|
+
pattern 1 above), or the dispatcher `useActionState` returns for it. A
|
|
917
|
+
client-side closure that calls the action is not a server reference and breaks
|
|
918
|
+
PE (see "File Uploads"). The simplest PE-safe shape:
|
|
748
919
|
|
|
749
920
|
```tsx
|
|
750
921
|
// Works with no-JS submission
|
|
@@ -764,7 +935,8 @@ export async function submitName(formData: FormData) {
|
|
|
764
935
|
|
|
765
936
|
`useActionState` and `useOptimistic` only enhance the experience once JS is
|
|
766
937
|
loaded — without JS, the underlying action still runs and the page still
|
|
767
|
-
re-renders
|
|
938
|
+
re-renders (with the `useActionState` result restored as its state). Don't rely
|
|
939
|
+
on client-only state for required form behavior.
|
|
768
940
|
|
|
769
941
|
## Cross-references
|
|
770
942
|
|