@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
|
@@ -6,13 +6,21 @@ argument-hint: [setup]
|
|
|
6
6
|
|
|
7
7
|
# Store-backed Document Cache
|
|
8
8
|
|
|
9
|
-
Caches complete HTTP responses (HTML
|
|
10
|
-
|
|
9
|
+
Caches complete HTTP responses (HTML documents and RSC payloads) in the
|
|
10
|
+
app-level cache store. Routes opt in by sending a `Cache-Control` header with
|
|
11
|
+
`s-maxage`; `stale-while-revalidate` adds a stale window. On a hit, nothing
|
|
12
|
+
below the middleware runs — no route middleware, handlers, or loaders.
|
|
13
|
+
|
|
14
|
+
Use it for pages whose **whole** response is public and identical for every
|
|
15
|
+
visitor sharing a URL, when you want the reuse to happen inside the app (so
|
|
16
|
+
outer middleware still runs and invalidation stays in Rango's tag system).
|
|
11
17
|
|
|
12
18
|
This middleware runs **inside** the worker/function. It is not itself a platform
|
|
13
|
-
CDN cache.
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
CDN cache. It works with every built-in store: with `CFCacheStore` the response
|
|
20
|
+
family uses Cloudflare's Cache API (and KV when configured), with
|
|
21
|
+
`VercelCacheStore` it uses Vercel Runtime Cache, and `MemorySegmentCacheStore`
|
|
22
|
+
keeps responses in process memory. The request still reaches Rango before the
|
|
23
|
+
middleware can return a store hit.
|
|
16
24
|
|
|
17
25
|
## Not this skill if…
|
|
18
26
|
|
|
@@ -27,9 +35,9 @@ reaches Rango before the middleware can return a store hit.
|
|
|
27
35
|
## Setup
|
|
28
36
|
|
|
29
37
|
Document caching is a middleware. Add `createDocumentCacheMiddleware()` to the
|
|
30
|
-
router with `.use()`.
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
router with `.use()`. It reads and writes the app-level store you configure on
|
|
39
|
+
`createRouter({ cache })`; the store is not a middleware option. Without an app
|
|
40
|
+
store, the middleware passes every request through.
|
|
33
41
|
|
|
34
42
|
```typescript
|
|
35
43
|
import { createRouter } from "@rangojs/router";
|
|
@@ -39,7 +47,7 @@ import {
|
|
|
39
47
|
} from "@rangojs/router/cache";
|
|
40
48
|
import { urlpatterns } from "./urls";
|
|
41
49
|
|
|
42
|
-
const router = createRouter<AppBindings>({
|
|
50
|
+
export const router = createRouter<AppBindings>({
|
|
43
51
|
document: Document,
|
|
44
52
|
urls: urlpatterns,
|
|
45
53
|
// App-level cache store. The document cache middleware uses this store's
|
|
@@ -53,13 +61,14 @@ router.use(
|
|
|
53
61
|
debug: process.env.NODE_ENV === "development",
|
|
54
62
|
}),
|
|
55
63
|
);
|
|
56
|
-
|
|
57
|
-
export default router;
|
|
58
64
|
```
|
|
59
65
|
|
|
66
|
+
Middleware registered before it runs on every request, hits included; everything
|
|
67
|
+
after it (and all route middleware) runs only on a miss.
|
|
68
|
+
|
|
60
69
|
## Route Opt-In with Cache-Control
|
|
61
70
|
|
|
62
|
-
Routes opt
|
|
71
|
+
Routes opt in to document caching by setting a `Cache-Control` response header
|
|
63
72
|
with `s-maxage`. The middleware caches responses whose `Cache-Control` includes
|
|
64
73
|
`s-maxage`; `stale-while-revalidate` enables background revalidation (SWR).
|
|
65
74
|
|
|
@@ -86,6 +95,21 @@ function BlogPostHandler(ctx) {
|
|
|
86
95
|
// Dashboard sets no Cache-Control header, so it is never document-cached.
|
|
87
96
|
```
|
|
88
97
|
|
|
98
|
+
A handler inside a `cache()` boundary or on a `ppr` route cannot write response
|
|
99
|
+
headers (the write throws). Set the header from route middleware instead — it
|
|
100
|
+
runs on every request that reaches the route:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
cache({ ttl: 60 }, () => [
|
|
104
|
+
path("/blog", BlogIndex, { name: "blog" }, () => [
|
|
105
|
+
middleware(async (ctx, next) => {
|
|
106
|
+
ctx.header("Cache-Control", "s-maxage=300, stale-while-revalidate=3600");
|
|
107
|
+
await next();
|
|
108
|
+
}),
|
|
109
|
+
]),
|
|
110
|
+
]);
|
|
111
|
+
```
|
|
112
|
+
|
|
89
113
|
## Document Cache Options
|
|
90
114
|
|
|
91
115
|
`createDocumentCacheMiddleware(options?)` accepts:
|
|
@@ -95,10 +119,11 @@ createDocumentCacheMiddleware({
|
|
|
95
119
|
// Skip specific paths (matched by pathname prefix)
|
|
96
120
|
skipPaths: ["/api", "/admin"],
|
|
97
121
|
|
|
98
|
-
// Custom cache key
|
|
99
|
-
|
|
122
|
+
// Custom cache key base. Replaces host + pathname + search entirely, so
|
|
123
|
+
// include the host yourself when one deployment serves several domains.
|
|
124
|
+
keyGenerator: (url) => `${url.host}${url.pathname}`,
|
|
100
125
|
|
|
101
|
-
// Conditional caching, evaluated per request
|
|
126
|
+
// Conditional caching, evaluated per request (receives the middleware ctx)
|
|
102
127
|
isEnabled: (ctx) => !ctx.request.headers.has("x-preview"),
|
|
103
128
|
|
|
104
129
|
// Debug logging (HIT, MISS, STALE, REVALIDATED)
|
|
@@ -150,18 +175,26 @@ header or logs to identify an actual CDN hit.
|
|
|
150
175
|
|
|
151
176
|
## Cache Key Generation
|
|
152
177
|
|
|
153
|
-
|
|
178
|
+
The default key is the host, pathname, and sorted search string (internal
|
|
179
|
+
`_rsc*` params removed, `cache.searchParams` filtering applied), followed by
|
|
180
|
+
suffixes that separate response variants:
|
|
154
181
|
|
|
155
|
-
- HTML requests: `{pathname}:html`
|
|
156
|
-
- RSC
|
|
182
|
+
- HTML document requests: `{host}{pathname}[?search]:html`
|
|
183
|
+
- RSC requests (client navigations and full-document Flight fetches):
|
|
184
|
+
`{host}{pathname}[?search]:rsc`
|
|
185
|
+
- Client navigations additionally add a hash of the segments the client already
|
|
186
|
+
has (`:{segmentHash}`), so navigations from pages with different layouts get
|
|
187
|
+
different cached payloads, and a `:fragments` marker when the client accepts
|
|
188
|
+
fragment envelopes.
|
|
157
189
|
|
|
158
|
-
|
|
190
|
+
A custom `keyGenerator(url)` replaces only the `{host}{pathname}[?search]` part;
|
|
191
|
+
the variant suffixes are still appended.
|
|
159
192
|
|
|
160
193
|
## What Gets Cached
|
|
161
194
|
|
|
162
195
|
- Full HTML responses (document requests)
|
|
163
196
|
- RSC payloads (client navigation)
|
|
164
|
-
- Only 200
|
|
197
|
+
- Only `GET` 200 responses whose `Cache-Control` includes `s-maxage`
|
|
165
198
|
|
|
166
199
|
## What's NOT Cached
|
|
167
200
|
|
|
@@ -169,7 +202,35 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
169
202
|
- Loader requests (`_rsc_loader`)
|
|
170
203
|
- Non-GET requests
|
|
171
204
|
- Responses without an `s-maxage` `Cache-Control` directive
|
|
205
|
+
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
206
|
+
unqualified `no-cache` (those win over `s-maxage` in a shared cache)
|
|
207
|
+
- Responses carrying `Set-Cookie` (a per-client value must not be replayed to
|
|
208
|
+
everyone)
|
|
172
209
|
- Non-200 responses
|
|
210
|
+
- A 200 whose render reported an error: a component that threw after the
|
|
211
|
+
response started streaming, such as an async server component whose fetch
|
|
212
|
+
failed. The status is already 200, so the body carries the error (an error
|
|
213
|
+
row in the RSC payload, an errored Suspense boundary in the HTML). The write
|
|
214
|
+
is skipped and logged as `[DocumentCache] cache write`; the error goes to
|
|
215
|
+
`onError` as `cache-write` unless the router already reported that same
|
|
216
|
+
error during the render. The current request still gets the errored render,
|
|
217
|
+
the next request renders fresh, and on a stale refresh the stale entry keeps
|
|
218
|
+
serving.
|
|
219
|
+
|
|
220
|
+
## Invalidation
|
|
221
|
+
|
|
222
|
+
Entries expire by `s-maxage` (+ `stale-while-revalidate`). They are also tagged
|
|
223
|
+
with the request's tag set, so `updateTag()`/`revalidateTag()` evict them:
|
|
224
|
+
|
|
225
|
+
- a render-callable `cacheTag("...")` in any server component on the page;
|
|
226
|
+
- tags of `cache()` segments, cached loaders, and `"use cache"` functions the
|
|
227
|
+
render read.
|
|
228
|
+
|
|
229
|
+
Tags are snapshotted after the response body has fully streamed, so tags from
|
|
230
|
+
Suspense-streamed content are included. A `Prerender()` route's build-time
|
|
231
|
+
segments contribute no tags (only its live loaders can), so such an entry
|
|
232
|
+
usually expires by TTL only.
|
|
233
|
+
See `/caching` → "Tag-Based Invalidation".
|
|
173
234
|
|
|
174
235
|
## Complete Example
|
|
175
236
|
|
|
@@ -179,7 +240,7 @@ import { createRouter } from "@rangojs/router";
|
|
|
179
240
|
import { createDocumentCacheMiddleware, CFCacheStore } from "@rangojs/router/cache";
|
|
180
241
|
import { urlpatterns } from "./urls";
|
|
181
242
|
|
|
182
|
-
const router = createRouter<AppBindings>({
|
|
243
|
+
export const router = createRouter<AppBindings>({
|
|
183
244
|
document: Document,
|
|
184
245
|
urls: urlpatterns,
|
|
185
246
|
cache: (_env, ctx) => ({ store: new CFCacheStore({ ctx: ctx! }) }),
|
|
@@ -192,7 +253,6 @@ router.use(
|
|
|
192
253
|
}),
|
|
193
254
|
);
|
|
194
255
|
|
|
195
|
-
export default router;
|
|
196
256
|
|
|
197
257
|
// urls.tsx
|
|
198
258
|
import { urls } from "@rangojs/router";
|
|
@@ -225,14 +285,18 @@ function BlogPost(ctx) {
|
|
|
225
285
|
}
|
|
226
286
|
```
|
|
227
287
|
|
|
288
|
+
Note that `BlogPostLoader` output is frozen into the cached response along with
|
|
289
|
+
everything else.
|
|
290
|
+
|
|
228
291
|
## Document Cache vs Segment Cache
|
|
229
292
|
|
|
230
|
-
| Feature | Document Cache
|
|
231
|
-
| ------------ |
|
|
232
|
-
| Granularity | Full response
|
|
233
|
-
| Opt-in | `Cache-Control` `s-maxage`
|
|
234
|
-
|
|
|
235
|
-
|
|
|
293
|
+
| Feature | Document Cache | Segment Cache (`cache()`) |
|
|
294
|
+
| ------------ | ------------------------------------- | --------------------------------- |
|
|
295
|
+
| Granularity | Full response | Individual segments |
|
|
296
|
+
| Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
|
|
297
|
+
| Loaders | Frozen with the response | Always run fresh |
|
|
298
|
+
| Use case | Static pages | Dynamic compositions |
|
|
299
|
+
| Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
|
|
236
300
|
|
|
237
301
|
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|
|
238
302
|
|
package/skills/fonts/SKILL.md
CHANGED
|
@@ -6,7 +6,10 @@ argument-hint: [provider]
|
|
|
6
6
|
|
|
7
7
|
# Fonts
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Three ways to load web fonts in a Rango app: a hosted provider (Google Fonts),
|
|
10
|
+
self-hosted files in `public/`, or Fontsource packages bundled by Vite (the
|
|
11
|
+
recommended default). Font links go in the Document `<head>` next to your
|
|
12
|
+
stylesheet, which follows the `?url` + `precedence` pattern from `/css`.
|
|
10
13
|
|
|
11
14
|
## Google Fonts
|
|
12
15
|
|
|
@@ -15,7 +18,7 @@ Load web fonts in the Document component with `<link rel="preload">` for optimal
|
|
|
15
18
|
"use client";
|
|
16
19
|
|
|
17
20
|
import type { ReactNode } from "react";
|
|
18
|
-
import {
|
|
21
|
+
import { Html } from "@rangojs/router/client";
|
|
19
22
|
import styles from "./index.css?url";
|
|
20
23
|
|
|
21
24
|
export function Document({ children }: { children: ReactNode }) {
|
|
@@ -30,16 +33,17 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
30
33
|
crossOrigin="anonymous"
|
|
31
34
|
/>
|
|
32
35
|
|
|
33
|
-
{/*
|
|
36
|
+
{/* Font stylesheet first, so app styles can override it */}
|
|
34
37
|
<link
|
|
35
38
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
36
39
|
rel="stylesheet"
|
|
40
|
+
precedence="default"
|
|
37
41
|
/>
|
|
38
42
|
|
|
39
43
|
{/* App styles */}
|
|
40
44
|
<link rel="preload" href={styles} as="style" />
|
|
41
|
-
<link rel="stylesheet" href={styles} />
|
|
42
|
-
<
|
|
45
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
46
|
+
<Html.Meta />
|
|
43
47
|
</head>
|
|
44
48
|
<body>{children}</body>
|
|
45
49
|
</html>
|
|
@@ -60,6 +64,8 @@ Or with Tailwind (see `/tailwind`):
|
|
|
60
64
|
|
|
61
65
|
```css
|
|
62
66
|
/* src/index.css */
|
|
67
|
+
@import "tailwindcss";
|
|
68
|
+
|
|
63
69
|
@theme {
|
|
64
70
|
--font-sans: "Inter", sans-serif;
|
|
65
71
|
}
|
|
@@ -92,7 +98,9 @@ body {
|
|
|
92
98
|
}
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
Preload the most critical weight in the Document
|
|
101
|
+
Preload the most critical weight in the Document (same imports as the Google
|
|
102
|
+
Fonts example above). Font preloads need `crossOrigin` even for same-origin
|
|
103
|
+
files, or the browser fetches the font twice:
|
|
96
104
|
|
|
97
105
|
```tsx
|
|
98
106
|
export function Document({ children }: { children: ReactNode }) {
|
|
@@ -107,8 +115,8 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
107
115
|
crossOrigin="anonymous"
|
|
108
116
|
/>
|
|
109
117
|
<link rel="preload" href={styles} as="style" />
|
|
110
|
-
<link rel="stylesheet" href={styles} />
|
|
111
|
-
<
|
|
118
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
119
|
+
<Html.Meta />
|
|
112
120
|
</head>
|
|
113
121
|
<body>{children}</body>
|
|
114
122
|
</html>
|
|
@@ -6,7 +6,7 @@ argument-hint: "[handler]"
|
|
|
6
6
|
|
|
7
7
|
# Handler-Attached `.use`
|
|
8
8
|
|
|
9
|
-
A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site.
|
|
9
|
+
A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site. Loader definitions from `createLoader()` accept a `.use` too, limited to `revalidate` and `cache` (see "Loaders" below).
|
|
10
10
|
|
|
11
11
|
This lets handlers be **self-contained, reusable units** — a page brings its own loader, a layout brings its own middleware, a parallel slot brings its own data + skeleton — without forcing every caller to wire the same items at every mount site.
|
|
12
12
|
|
|
@@ -51,13 +51,16 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
51
51
|
|
|
52
52
|
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
53
|
|
|
54
|
-
| Mount site | Allowed item types
|
|
55
|
-
| ------------------------------------------------- |
|
|
56
|
-
| `path()`
|
|
57
|
-
| `layout()` | All of the above, plus `route
|
|
58
|
-
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition`
|
|
59
|
-
| `intercept()` | `middleware`, `
|
|
60
|
-
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache`
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `middleware`, `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `revalidate` goes on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
|
+
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
|
|
62
|
+
|
|
63
|
+
`route` is the item type a `path()` call produces.
|
|
61
64
|
|
|
62
65
|
For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
|
|
63
66
|
|
|
@@ -68,7 +71,13 @@ handler.use() returned middleware() which is not valid inside parallel().
|
|
|
68
71
|
Allowed types: revalidate, loader, loading, errorBoundary, notFoundBoundary, transition.
|
|
69
72
|
```
|
|
70
73
|
|
|
71
|
-
The narrowest contract is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
|
|
74
|
+
The narrowest contract for a handler is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
|
|
75
|
+
|
|
76
|
+
This runtime check applies to items returned by `handler.use()`. Items in an explicit mount-site `use()` callback are constrained by the TypeScript use-item types (e.g. a `parallel()` callback is typed to parallel items), not by this runtime allow-list. `intercept()` is the exception: it checks its merged items, explicit `use()` and `handler.use()` together, against the same list at runtime (`validateInterceptUseItems`), and a rejected item throws with a pointer instead:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
errorBoundary() is not valid inside intercept("@modal", "product") use() (including the handler's .use): put it on the enclosing layout or path; it handles the intercept's handler and loader errors.
|
|
80
|
+
```
|
|
72
81
|
|
|
73
82
|
## Composition with explicit `use()`
|
|
74
83
|
|
|
@@ -281,7 +290,7 @@ AdminLayout.use = () => [
|
|
|
281
290
|
|
|
282
291
|
### Intercepts (`intercept()`)
|
|
283
292
|
|
|
284
|
-
Intercept handlers can carry their own middleware chain, loaders, and
|
|
293
|
+
Intercept handlers can carry their own middleware chain, loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. Boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)).
|
|
285
294
|
|
|
286
295
|
```typescript
|
|
287
296
|
const QuickViewModal: Handler = async (ctx) => {
|
|
@@ -295,6 +304,30 @@ QuickViewModal.use = () => [
|
|
|
295
304
|
];
|
|
296
305
|
```
|
|
297
306
|
|
|
307
|
+
### Loaders (`loader()`)
|
|
308
|
+
|
|
309
|
+
A loader definition can carry its own cache and revalidation defaults. They are
|
|
310
|
+
merged into every `loader(Def)` registration, before the explicit
|
|
311
|
+
`loader(Def, () => [...])` items:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { cache, createLoader, revalidate } from "@rangojs/router";
|
|
315
|
+
import * as CatalogActions from "./actions/catalog";
|
|
316
|
+
|
|
317
|
+
export const CategoriesLoader = createLoader(async () => fetchCategories());
|
|
318
|
+
CategoriesLoader.use = () => [
|
|
319
|
+
cache({ ttl: 300 }),
|
|
320
|
+
// Loaders re-run after every action by default. Re-run this one only for
|
|
321
|
+
// catalog actions; on navigation, undefined keeps the default.
|
|
322
|
+
revalidate((ctx) =>
|
|
323
|
+
ctx.isAction() ? ctx.isAction(CatalogActions) : undefined,
|
|
324
|
+
),
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
// Mount sites get the cache + revalidation defaults with no extra wiring.
|
|
328
|
+
layout(<ShopLayout />, () => [loader(CategoriesLoader) /* … */]);
|
|
329
|
+
```
|
|
330
|
+
|
|
298
331
|
## `loading()` is a single-assignment item — scope it correctly
|
|
299
332
|
|
|
300
333
|
Most `use` items accumulate when merged: `handler.use` `middleware()` runs _and_ explicit `middleware()` runs; both `loader()` registrations apply. `loading()` is different — it mutates `entry.loading` directly, last call wins ([dsl-helpers.ts `loading`](../../src/route-definition/dsl-helpers.ts)).
|
|
@@ -310,7 +343,7 @@ Cart.use = () => [loader(CartLoader), loading(<CartSkeleton />)];
|
|
|
310
343
|
const Notifs: Handler = async (ctx) => { /* … */ };
|
|
311
344
|
Notifs.use = () => [loader(NotifsLoader), loading(<NotifsSkeleton />)];
|
|
312
345
|
|
|
313
|
-
//
|
|
346
|
+
// @cart gets a custom skeleton; @notifs keeps its handler.use default.
|
|
314
347
|
parallel({
|
|
315
348
|
"@cart": {
|
|
316
349
|
handler: Cart,
|
|
@@ -319,7 +352,7 @@ parallel({
|
|
|
319
352
|
"@notifs": Notifs,
|
|
320
353
|
});
|
|
321
354
|
|
|
322
|
-
//
|
|
355
|
+
// Opt one slot out of streaming while siblings still stream the broadcast.
|
|
323
356
|
parallel(
|
|
324
357
|
{
|
|
325
358
|
"@cart": { handler: Cart, use: () => [loading(false)] },
|
|
@@ -333,7 +366,7 @@ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-
|
|
|
333
366
|
|
|
334
367
|
Other things to keep in mind about `loading()`:
|
|
335
368
|
|
|
336
|
-
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading
|
|
369
|
+
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/fresh.ts](../../src/router/segment-resolution/fresh.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
|
|
337
370
|
|
|
338
371
|
Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
|
|
339
372
|
|
|
@@ -343,10 +376,13 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
343
376
|
- **Branded handlers** — `Static()`, `Prerender()`, and `Passthrough()` are positional constructors (not object-arg). Construct first, then attach `.use` to the returned definition:
|
|
344
377
|
|
|
345
378
|
```typescript
|
|
346
|
-
const ProductPage = Prerender(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
379
|
+
const ProductPage = Prerender(
|
|
380
|
+
async () => [{ slug: "shoes" }, { slug: "jacket" }], // getParams
|
|
381
|
+
async (ctx) => {
|
|
382
|
+
const product = await fetchProduct(ctx.params.slug);
|
|
383
|
+
return <ProductView product={product} />;
|
|
384
|
+
},
|
|
385
|
+
);
|
|
350
386
|
ProductPage.use = () => [loader(ProductLoader)];
|
|
351
387
|
```
|
|
352
388
|
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -6,8 +6,14 @@ argument-hint: [hook-name]
|
|
|
6
6
|
|
|
7
7
|
# Client-Side React Hooks
|
|
8
8
|
|
|
9
|
+
The client-side API for reading router state in client components: navigation
|
|
10
|
+
state and actions, URL and params, loader data, handles, action status,
|
|
11
|
+
location state, outlets, and client-cache invalidation.
|
|
12
|
+
|
|
9
13
|
Import the hooks and components in this skill from `@rangojs/router/client`.
|
|
10
|
-
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types
|
|
14
|
+
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types;
|
|
15
|
+
the exceptions used here are `invalidateClientCache()` (root only) and
|
|
16
|
+
`createLocationState()` (exported from both).
|
|
11
17
|
|
|
12
18
|
## Not this skill if…
|
|
13
19
|
|
|
@@ -16,6 +22,8 @@ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
|
|
|
16
22
|
resolved.
|
|
17
23
|
- You want to control when a loader re-runs after an action — that is
|
|
18
24
|
`revalidate()` in the server DSL: see `/loader`.
|
|
25
|
+
- You want an action to leave the client cache alone — that is
|
|
26
|
+
`keepClientCache()` inside the action: see `/server-actions`.
|
|
19
27
|
|
|
20
28
|
Each hook's full API and recipe lives in a companion file linked below. Read
|
|
21
29
|
the one for your case.
|
|
@@ -65,22 +73,22 @@ the one for your case.
|
|
|
65
73
|
|
|
66
74
|
## Hook Summary
|
|
67
75
|
|
|
68
|
-
| Hook | Purpose | Returns
|
|
69
|
-
| ------------------------- | -------------------------------------------------------------- |
|
|
70
|
-
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
71
|
-
| `usePathname()` | Current pathname | `string`
|
|
72
|
-
| `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple)
|
|
73
|
-
| `useHref()` | Mount-aware href | `(path) => string`
|
|
74
|
-
| `useMount()` | Current include() mount path | `string`
|
|
75
|
-
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string`
|
|
76
|
-
| `useNavigation()` | Reactive navigation state | state, location, isStreaming
|
|
77
|
-
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward
|
|
78
|
-
| `useSegments()` | URL path & segment IDs | path, segmentIds, location
|
|
79
|
-
| `useLinkStatus()` | Link pending state | { pending }
|
|
80
|
-
| `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch
|
|
81
|
-
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch
|
|
82
|
-
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>`
|
|
83
|
-
| `useHandle()` | Accumulated handle data | T (handle type)
|
|
84
|
-
| `useAction()` | Server action state | state, error, result
|
|
85
|
-
| `useLocationState()` | History state (persists or flash) | T \| undefined
|
|
86
|
-
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void`
|
|
76
|
+
| Hook | Purpose | Returns |
|
|
77
|
+
| ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
78
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string \| undefined>`) or selected value |
|
|
79
|
+
| `usePathname()` | Current pathname | `string` |
|
|
80
|
+
| `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple) |
|
|
81
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
82
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
83
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
84
|
+
| `useNavigation()` | Reactive navigation state | state, location, pendingUrl, isStreaming |
|
|
85
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
86
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
87
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
88
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch |
|
|
89
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch |
|
|
90
|
+
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
|
|
91
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
92
|
+
| `useAction()` | Server action state | state, actionId, payload, error, result |
|
|
93
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
94
|
+
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
|
package/skills/hooks/data.md
CHANGED
|
@@ -36,9 +36,15 @@ same-route nav, a same-structure search/filter nav), the reader that is still
|
|
|
36
36
|
showing the OLD data reports `isLoading: true` from
|
|
37
37
|
the moment the new tree is committed until the new data lands — render your
|
|
38
38
|
stale indicator from it. The flag never flashes back to `false` on the old
|
|
39
|
-
data: it flips in the same commit that swaps `data`.
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
data: it flips in the same commit that swaps `data`. The pin is per loader
|
|
40
|
+
family (`loader.$$id`), not per segment: a navigation that re-runs
|
|
41
|
+
`CartLoader` flags every `useLoader(CartLoader)`, including a persisting
|
|
42
|
+
layout reader whose own copy is not replaced. That is "this loader is in
|
|
43
|
+
flight," not "this segment's copy is being replaced." A _different_ layout
|
|
44
|
+
loader the server does not re-execute stays `isLoading: false`. A child can
|
|
45
|
+
already read a layout registration via the context walk; a second
|
|
46
|
+
`loader(Same)` on the child is a second run of the same family, not a
|
|
47
|
+
separate loading flag. It never applies to a
|
|
42
48
|
navigation that remounts the route (that reader is gone; the skeleton shows),
|
|
43
49
|
to a fully-prefetched nav (its data is already settled when it commits, so
|
|
44
50
|
nothing is pending to flag), or to an ephemeral `useFetchLoader` read outside
|
|
@@ -51,8 +57,19 @@ function ProductPrice() {
|
|
|
51
57
|
}
|
|
52
58
|
```
|
|
53
59
|
|
|
60
|
+
Unit-test the indicator with `renderRoute` (`/testing`, client-components.md
|
|
61
|
+
"Held navigation"): a `transition` spec plus a pending seed in
|
|
62
|
+
`router.navigate(url, { loaders })`.
|
|
63
|
+
|
|
54
64
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
55
65
|
|
|
66
|
+
**`load()` / `refetch` need a fetchable loader.** `load()`, `refetch`, and
|
|
67
|
+
`useRefreshLoaders()` call the loader through the `_rsc_loader` endpoint, which
|
|
68
|
+
only serves loaders created with `createLoader(fn, true)` or
|
|
69
|
+
`createLoader(fn, { middleware })`. A plain `createLoader(fn)` answers 403 there;
|
|
70
|
+
it re-runs only as part of a render (navigation, `router.refresh()`, or the
|
|
71
|
+
revalidation after a server action). See `/loader` → "Fetchable Loaders".
|
|
72
|
+
|
|
56
73
|
Loaders can also be passed as props from server to client components:
|
|
57
74
|
|
|
58
75
|
```tsx
|
|
@@ -108,6 +125,7 @@ reading the same loader id. Layout, page, and parallel-slot reads
|
|
|
108
125
|
all converge on the new value:
|
|
109
126
|
|
|
110
127
|
```tsx
|
|
128
|
+
// CartLoader = createLoader(fn, true), registered with loader(CartLoader).
|
|
111
129
|
// Layout button calls load() — the page read below sees the update too.
|
|
112
130
|
function Layout() {
|
|
113
131
|
const { data, load } = useLoader(CartLoader);
|
|
@@ -262,9 +280,11 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
|
|
|
262
280
|
|
|
263
281
|
```tsx
|
|
264
282
|
"use client";
|
|
283
|
+
import { useRef } from "react";
|
|
265
284
|
import { useFetchLoader } from "@rangojs/router/client";
|
|
266
285
|
import { FileUploadLoader } from "../loaders/upload";
|
|
267
286
|
|
|
287
|
+
// Needs JavaScript: the form action is a client function, not a server action.
|
|
268
288
|
function FileUploader() {
|
|
269
289
|
const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
|
|
270
290
|
const formRef = useRef<HTMLFormElement>(null);
|
|
@@ -40,12 +40,16 @@ re-renders on every handle update, late loader pushes included.
|
|
|
40
40
|
Handles can be passed as props from server to client components:
|
|
41
41
|
|
|
42
42
|
```tsx
|
|
43
|
-
// Server component
|
|
44
|
-
path(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
43
|
+
// Server component (route handler)
|
|
44
|
+
path(
|
|
45
|
+
"/dashboard",
|
|
46
|
+
(ctx) => {
|
|
47
|
+
const push = ctx.use(Breadcrumbs);
|
|
48
|
+
push({ label: "Dashboard", href: "/dashboard" });
|
|
49
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
50
|
+
},
|
|
51
|
+
{ name: "dashboard" },
|
|
52
|
+
);
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
```tsx
|
|
@@ -58,7 +62,9 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
|
58
62
|
return (
|
|
59
63
|
<nav>
|
|
60
64
|
{crumbs.map((c) => (
|
|
61
|
-
<a
|
|
65
|
+
<a key={c.href} href={c.href}>
|
|
66
|
+
{c.label}
|
|
67
|
+
</a>
|
|
62
68
|
))}
|
|
63
69
|
</nav>
|
|
64
70
|
);
|
|
@@ -78,33 +84,42 @@ tracking actions called outside a `<form action={...}>` flow.
|
|
|
78
84
|
|
|
79
85
|
### useAction()
|
|
80
86
|
|
|
81
|
-
Track
|
|
87
|
+
Track the lifecycle of a server action, wherever on the page it was invoked.
|
|
88
|
+
If the same action fires several times, the hook reports the latest call.
|
|
82
89
|
|
|
83
90
|
```tsx
|
|
84
91
|
"use client";
|
|
85
92
|
import { useAction } from "@rangojs/router/client";
|
|
86
|
-
import { addToCart } from "../actions/cart";
|
|
93
|
+
import { addToCart } from "../actions/cart"; // addToCart(productId: string)
|
|
87
94
|
|
|
88
95
|
function AddToCartButton({ productId }: { productId: string }) {
|
|
89
96
|
const { state, error, result } = useAction(addToCart);
|
|
90
97
|
|
|
91
98
|
// state: 'idle' | 'loading' | 'streaming'
|
|
92
99
|
// actionId: string | null
|
|
93
|
-
// payload: unknown | null (
|
|
94
|
-
// error:
|
|
100
|
+
// payload: unknown[] | FormData | null (the call's arguments)
|
|
101
|
+
// error: unknown | null (whatever the action threw)
|
|
95
102
|
// result: unknown | null (return value)
|
|
96
103
|
|
|
97
104
|
return (
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
// .bind keeps the form progressively enhanced; useAction still matches it
|
|
106
|
+
<form action={addToCart.bind(null, productId)}>
|
|
100
107
|
<button disabled={state === "loading"}>
|
|
101
108
|
{state === "loading" ? "Adding..." : "Add to Cart"}
|
|
102
109
|
</button>
|
|
103
|
-
{error && <p className="error">{error.message}</p>}
|
|
110
|
+
{error instanceof Error && <p className="error">{error.message}</p>}
|
|
104
111
|
</form>
|
|
105
112
|
);
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
//
|
|
109
|
-
const isLoading = useAction(
|
|
115
|
+
// With a selector (re-renders only when the selected value changes)
|
|
116
|
+
const isLoading = useAction(addToCart, (s) => s.state === "loading");
|
|
117
|
+
|
|
118
|
+
// Match by export name (suffix match on "#addToCart"; ambiguous if two
|
|
119
|
+
// "use server" files export the same name)
|
|
120
|
+
const isAdding = useAction("addToCart", (s) => s.state === "loading");
|
|
110
121
|
```
|
|
122
|
+
|
|
123
|
+
Pass an action imported directly into the client component, or its export
|
|
124
|
+
name as a string. An action received as a prop from a server component has
|
|
125
|
+
lost the metadata `useAction` needs and throws; use the string form there.
|