@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
|
@@ -8,6 +8,18 @@ argument-hint: [@slot-name] [route-to-intercept]
|
|
|
8
8
|
|
|
9
9
|
Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
|
|
10
10
|
|
|
11
|
+
`intercept(slot, routeName, handler, config?, use?)`:
|
|
12
|
+
|
|
13
|
+
- `slot` — the `@name` slot the intercept renders into; the layout places it
|
|
14
|
+
with `<ParallelOutlet name="@modal" />`.
|
|
15
|
+
- `routeName` — the target route. A dot prefix (`".product"`) resolves inside
|
|
16
|
+
the current `include()` scope; a bare name (`"product"`) is the global route
|
|
17
|
+
name. Declare the intercept in a layout that wraps the target route — the
|
|
18
|
+
router finds intercepts by walking up the target route's own layout chain.
|
|
19
|
+
- `handler` — the component or handler rendered in the slot.
|
|
20
|
+
- `config` — optional `{ when }` (see "Conditional Intercept"); pass the `use`
|
|
21
|
+
callback as the 4th argument when there is no config.
|
|
22
|
+
|
|
11
23
|
## Not this skill if…
|
|
12
24
|
|
|
13
25
|
- You want a slot that ALWAYS renders alongside the page (sidebar, multi-column
|
|
@@ -60,7 +72,8 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
60
72
|
|
|
61
73
|
## Intercept with Layout
|
|
62
74
|
|
|
63
|
-
Wrap intercept content in a modal layout
|
|
75
|
+
Wrap intercept content in a modal layout. The layout renders the intercept
|
|
76
|
+
content through `<Outlet />` (not a `children` prop):
|
|
64
77
|
|
|
65
78
|
```typescript
|
|
66
79
|
intercept(
|
|
@@ -68,21 +81,29 @@ intercept(
|
|
|
68
81
|
"product",
|
|
69
82
|
<ProductModalContent />,
|
|
70
83
|
() => [
|
|
71
|
-
layout(<ModalWrapper />), // Wraps the modal content
|
|
84
|
+
layout(<ModalWrapper />), // Wraps the modal content via <Outlet />
|
|
72
85
|
loader(ProductLoader),
|
|
73
86
|
loading(<ProductModalSkeleton />),
|
|
74
87
|
]
|
|
75
88
|
)
|
|
76
89
|
```
|
|
77
90
|
|
|
91
|
+
Only the first `layout()` in an intercept's `use()` is applied. It is modal
|
|
92
|
+
chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
|
|
93
|
+
Attach loaders and the rest to the intercept itself.
|
|
94
|
+
|
|
78
95
|
## Intercept Middleware
|
|
79
96
|
|
|
80
97
|
Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
|
|
81
98
|
|
|
82
99
|
```
|
|
83
|
-
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept
|
|
100
|
+
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept loaders + handler
|
|
84
101
|
```
|
|
85
102
|
|
|
103
|
+
The intercept's loaders are started before its handler is called, so they run
|
|
104
|
+
in parallel with it. If intercept middleware returns a `Response` (e.g. a
|
|
105
|
+
redirect), the intercept stops there.
|
|
106
|
+
|
|
86
107
|
```typescript
|
|
87
108
|
intercept(
|
|
88
109
|
"@modal",
|
|
@@ -108,22 +129,55 @@ If an intercept depends on data established by an outer layout/handler,
|
|
|
108
129
|
revalidate that outer segment too or reload/guard the data inside the
|
|
109
130
|
intercept.
|
|
110
131
|
|
|
132
|
+
### Revalidating an Intercept
|
|
133
|
+
|
|
134
|
+
An intercept has no segment-level `revalidate()`. Its handler and layout
|
|
135
|
+
re-render every time the intercept resolves live — each intercepted
|
|
136
|
+
navigation, and each action submitted while the modal is open. When the
|
|
137
|
+
segment cache or a prerendered intercept variant serves the modal, the stored
|
|
138
|
+
handler output is reused and only the loaders run. Each loader decides for
|
|
139
|
+
itself through its own `revalidate()`, with the normal loader defaults.
|
|
140
|
+
|
|
141
|
+
A `revalidate()` placed directly in the intercept's `use()` (or returned from
|
|
142
|
+
its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Throws: revalidate() is not valid inside intercept("@modal", "product") use()
|
|
146
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
147
|
+
revalidate(() => false),
|
|
148
|
+
loader(ProductLoader),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Correct: the loader owns the decision
|
|
152
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
153
|
+
loader(ProductLoader, () => [revalidate(() => false)]),
|
|
154
|
+
]);
|
|
155
|
+
```
|
|
156
|
+
|
|
111
157
|
### Revalidation Contracts for Intercept Dependencies
|
|
112
158
|
|
|
113
|
-
|
|
114
|
-
|
|
159
|
+
When an outer layout produces `ctx.set()` data that the intercept reads, the
|
|
160
|
+
producer is the side that needs a contract: outer layouts are skipped during
|
|
161
|
+
partial action revalidation unless they opt in. The intercept side needs none
|
|
162
|
+
by default — its handler re-renders on every action while the modal is open,
|
|
163
|
+
and its loaders re-run after every action. If you narrow an intercept loader
|
|
164
|
+
with its own `revalidate()`, compose the producer's contract into it so the
|
|
165
|
+
two stay in step:
|
|
115
166
|
|
|
116
167
|
```typescript
|
|
168
|
+
import type { Revalidate } from "@rangojs/router";
|
|
117
169
|
import * as ProductActions from "./actions/product";
|
|
118
170
|
|
|
119
|
-
export const revalidateProductShell = (ctx) =>
|
|
171
|
+
export const revalidateProductShell: Revalidate = (ctx) =>
|
|
120
172
|
ctx.isAction(ProductActions) || undefined;
|
|
121
173
|
|
|
122
174
|
layout(ProductLayout, () => [
|
|
123
175
|
revalidate(revalidateProductShell), // producer reruns
|
|
124
176
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
loader(ProductLoader, () => [
|
|
178
|
+
revalidate(revalidateProductShell), // reruns with the producer
|
|
179
|
+
revalidate(() => false), // otherwise keep the loaded data
|
|
180
|
+
]),
|
|
127
181
|
]),
|
|
128
182
|
]);
|
|
129
183
|
```
|
|
@@ -143,17 +197,16 @@ export const revalidateProduct = () => [
|
|
|
143
197
|
layout(ProductLayout, () => [
|
|
144
198
|
revalidateProduct(),
|
|
145
199
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
146
|
-
revalidateProduct(),
|
|
147
|
-
loader(ProductLoader),
|
|
200
|
+
loader(ProductLoader, () => [revalidateProduct(), revalidate(() => false)]),
|
|
148
201
|
]),
|
|
149
202
|
]);
|
|
150
203
|
```
|
|
151
204
|
|
|
152
205
|
## Conditional Intercept with the `when` config
|
|
153
206
|
|
|
154
|
-
Only intercept based on navigation context. `when`
|
|
155
|
-
(
|
|
156
|
-
|
|
207
|
+
Only intercept based on navigation context. `when` goes in the config object
|
|
208
|
+
(4th argument); the use-items then move to the 5th-argument callback. The
|
|
209
|
+
config type is not exported — pass an object literal.
|
|
157
210
|
|
|
158
211
|
```typescript
|
|
159
212
|
intercept(
|
|
@@ -168,9 +221,19 @@ intercept(
|
|
|
168
221
|
)
|
|
169
222
|
```
|
|
170
223
|
|
|
171
|
-
`when` is a match-time selector
|
|
224
|
+
`when` is a synchronous match-time selector. It receives `from` / `to` (URLs),
|
|
225
|
+
`fromRouteName` / `toRouteName` (named routes only), `params` (the target's),
|
|
226
|
+
`segments` (the client's current segment path and ids), `request`, and `env`.
|
|
172
227
|
Pass an array of predicates for AND logic (all must return true). Omit `when`
|
|
173
|
-
entirely and the intercept always activates.
|
|
228
|
+
entirely and the intercept always activates. `when` is not re-evaluated during
|
|
229
|
+
action revalidation, so an open modal stays open after an action.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Intercept only when opened from the shop index
|
|
233
|
+
intercept("@modal", "product", <ProductModal />, {
|
|
234
|
+
when: ({ fromRouteName }) => fromRouteName === "index",
|
|
235
|
+
})
|
|
236
|
+
```
|
|
174
237
|
|
|
175
238
|
```typescript
|
|
176
239
|
intercept(
|
|
@@ -204,22 +267,84 @@ intercept(
|
|
|
204
267
|
)
|
|
205
268
|
```
|
|
206
269
|
|
|
270
|
+
## What an Intercept's `use()` Accepts
|
|
271
|
+
|
|
272
|
+
An intercept's `use()` (and its handler's `.use`) accepts:
|
|
273
|
+
|
|
274
|
+
- `loader()`, with its own `revalidate()` and `cache()` inside it
|
|
275
|
+
- `middleware()`
|
|
276
|
+
- `loading()`
|
|
277
|
+
- `layout(Chrome)`: the modal chrome, without `use()` items of its own
|
|
278
|
+
- `route`, the item type a `path()` call produces
|
|
279
|
+
- `transition()`
|
|
280
|
+
|
|
281
|
+
Everything else throws at definition time with a pointer to where it goes:
|
|
282
|
+
`revalidate()` onto the loader, `errorBoundary()` / `notFoundBoundary()` onto
|
|
283
|
+
the enclosing layout (see below), and `cache()` onto the target route.
|
|
284
|
+
|
|
285
|
+
## Caching an Intercept
|
|
286
|
+
|
|
287
|
+
To cache a modal, put `cache()` on the target route (or a layout above it).
|
|
288
|
+
An intercept navigation is cached under that scope with its own `intercept:`
|
|
289
|
+
key, so it never collides with the full-page render of the same URL. For
|
|
290
|
+
finer control, use `"use cache"` in the intercept handler, or cache the
|
|
291
|
+
intercept's loader: `loader(ProductLoader, () => [cache()])`.
|
|
292
|
+
|
|
293
|
+
## Errors in an Intercept
|
|
294
|
+
|
|
295
|
+
An intercept has no boundary of its own. When the intercept's handler or one
|
|
296
|
+
of its loaders throws, or calls `notFound()`, the modal slot renders the
|
|
297
|
+
`errorBoundary()` / `notFoundBoundary()` of the layout or path that declares the
|
|
298
|
+
intercept, or the nearest ancestor that has one. That holds when the declaring
|
|
299
|
+
layout has no routes of its own, too: the lookup continues at the layout that
|
|
300
|
+
holds it and that layout's ancestors. The intercepted route's own
|
|
301
|
+
boundaries are not consulted: the modal renders in the declaring layout's slot,
|
|
302
|
+
not in the route. On a handler error the modal layout and the background page
|
|
303
|
+
stay mounted, and the status is 500 / 404. A thrown `Response` (`redirect()`)
|
|
304
|
+
still short-circuits the navigation.
|
|
305
|
+
|
|
306
|
+
With `loading()`, an async handler streams after the response has started, so
|
|
307
|
+
its rejection is not caught on the server. Wrap the modal layout's `<Outlet />`
|
|
308
|
+
in a client error boundary.
|
|
309
|
+
|
|
310
|
+
An `errorBoundary()` or `notFoundBoundary()` placed directly in the intercept's
|
|
311
|
+
`use()` (or returned from its handler's `.use`) throws at definition time. Put
|
|
312
|
+
it on the enclosing layout:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Throws: errorBoundary() is not valid inside intercept("@modal", "product") use()
|
|
316
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
317
|
+
loader(ProductLoader),
|
|
318
|
+
errorBoundary(<ModalError />),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
// Correct: the declaring layout's boundaries handle the intercept's handler and loaders
|
|
322
|
+
layout(<ShopLayout />, () => [
|
|
323
|
+
errorBoundary(<ShopError />),
|
|
324
|
+
notFoundBoundary(<ProductNotFound />),
|
|
325
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
326
|
+
loader(ProductLoader),
|
|
327
|
+
]),
|
|
328
|
+
]);
|
|
329
|
+
```
|
|
330
|
+
|
|
207
331
|
## Closing the Modal
|
|
208
332
|
|
|
209
333
|
Use navigation to close:
|
|
210
334
|
|
|
211
335
|
```typescript
|
|
212
336
|
"use client";
|
|
213
|
-
import { useRouter } from "@rangojs/router/client";
|
|
337
|
+
import { Outlet, useRouter } from "@rangojs/router/client";
|
|
214
338
|
|
|
215
|
-
|
|
339
|
+
// Used as the intercept's layout: layout(<ModalWrapper />)
|
|
340
|
+
export function ModalWrapper() {
|
|
216
341
|
const router = useRouter();
|
|
217
342
|
|
|
218
343
|
return (
|
|
219
344
|
<div className="modal-overlay" onClick={() => router.back()}>
|
|
220
345
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
221
346
|
<button onClick={() => router.back()}>Close</button>
|
|
222
|
-
|
|
347
|
+
<Outlet />
|
|
223
348
|
</div>
|
|
224
349
|
</div>
|
|
225
350
|
);
|
|
@@ -305,10 +430,10 @@ segments.
|
|
|
305
430
|
## Complete Example
|
|
306
431
|
|
|
307
432
|
```typescript
|
|
308
|
-
// components/
|
|
433
|
+
// components/shop-layout.tsx
|
|
309
434
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
310
435
|
|
|
311
|
-
function ShopLayout() {
|
|
436
|
+
export function ShopLayout() {
|
|
312
437
|
return (
|
|
313
438
|
<div className="shop">
|
|
314
439
|
<ParallelOutlet name="@promoBanner" />
|
|
@@ -320,16 +445,23 @@ function ShopLayout() {
|
|
|
320
445
|
);
|
|
321
446
|
}
|
|
322
447
|
|
|
323
|
-
|
|
448
|
+
// components/modal-wrapper.tsx (see "Closing the Modal" for a closable version)
|
|
449
|
+
import { Outlet } from "@rangojs/router/client";
|
|
450
|
+
|
|
451
|
+
export function ModalWrapper() {
|
|
324
452
|
return (
|
|
325
453
|
<div className="modal-overlay">
|
|
326
|
-
<div className="modal">
|
|
454
|
+
<div className="modal">
|
|
455
|
+
<Outlet />
|
|
456
|
+
</div>
|
|
327
457
|
</div>
|
|
328
458
|
);
|
|
329
459
|
}
|
|
330
460
|
|
|
331
|
-
// urls/shop.tsx
|
|
461
|
+
// urls/shop.tsx — mounted with include("/shop", shopPatterns, { name: "shop" })
|
|
332
462
|
import { urls } from "@rangojs/router";
|
|
463
|
+
import { ShopLayout } from "../components/shop-layout";
|
|
464
|
+
import { ModalWrapper } from "../components/modal-wrapper";
|
|
333
465
|
|
|
334
466
|
export const shopPatterns = urls(({
|
|
335
467
|
path,
|
|
@@ -338,6 +470,7 @@ export const shopPatterns = urls(({
|
|
|
338
470
|
intercept,
|
|
339
471
|
loader,
|
|
340
472
|
loading,
|
|
473
|
+
cache,
|
|
341
474
|
}) => [
|
|
342
475
|
layout(<ShopLayout />, () => [
|
|
343
476
|
parallel({
|
|
@@ -347,7 +480,7 @@ export const shopPatterns = urls(({
|
|
|
347
480
|
// Intercept product detail into modal
|
|
348
481
|
intercept(
|
|
349
482
|
"@modal",
|
|
350
|
-
"product",
|
|
483
|
+
".product", // dot-local: resolves to "shop.product" in this include
|
|
351
484
|
<ProductModalContent />,
|
|
352
485
|
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
353
486
|
() => [
|
|
@@ -370,9 +503,11 @@ export const shopPatterns = urls(({
|
|
|
370
503
|
|
|
371
504
|
## Handler-attached `.use`
|
|
372
505
|
|
|
373
|
-
Intercept handlers can carry their own middleware, loaders, loading state,
|
|
506
|
+
Intercept handlers can carry their own middleware, loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
|
|
374
507
|
|
|
375
508
|
```typescript
|
|
509
|
+
import { layout, loader, loading, type Handler } from "@rangojs/router";
|
|
510
|
+
|
|
376
511
|
const QuickViewModal: Handler = async (ctx) => {
|
|
377
512
|
const product = await ctx.use(ProductLoader);
|
|
378
513
|
return <QuickView product={product} />;
|
|
@@ -380,7 +515,7 @@ const QuickViewModal: Handler = async (ctx) => {
|
|
|
380
515
|
QuickViewModal.use = () => [
|
|
381
516
|
loader(ProductLoader),
|
|
382
517
|
loading(<QuickViewSkeleton />),
|
|
383
|
-
layout(<ModalChrome />),
|
|
518
|
+
layout(<ModalChrome />), // ModalChrome renders <Outlet />
|
|
384
519
|
];
|
|
385
520
|
|
|
386
521
|
intercept("@modal", "product", QuickViewModal);
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -6,7 +6,15 @@ argument-hint: [component]
|
|
|
6
6
|
|
|
7
7
|
# Layouts with layout()
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`layout(handler, use?)` wraps its child routes in shared UI (nav, sidebar,
|
|
10
|
+
shell). The child content renders where the layout places `<Outlet />`, and the
|
|
11
|
+
layout stays mounted while you navigate between the routes it wraps. This skill
|
|
12
|
+
covers layout forms, outlets, orphan layouts (a layout inside a `path()`), and
|
|
13
|
+
layout revalidation.
|
|
14
|
+
|
|
15
|
+
Related skills: `/parallel` (named slots rendered by a layout), `/intercept`
|
|
16
|
+
(modal slots), `/route` (the routes a layout wraps), `/router-setup` (the
|
|
17
|
+
`document` shell that renders `<html>`).
|
|
10
18
|
|
|
11
19
|
## Basic Layout
|
|
12
20
|
|
|
@@ -50,7 +58,12 @@ layout(ShopLayout, () => [path("/shop", ShopIndex, { name: "shop" })]);
|
|
|
50
58
|
|
|
51
59
|
### Handler with Context
|
|
52
60
|
|
|
61
|
+
A function layout receives the same handler context as a route (`ctx.params`,
|
|
62
|
+
`ctx.get()`, `ctx.set()`, `ctx.use()`):
|
|
63
|
+
|
|
53
64
|
```typescript
|
|
65
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
66
|
+
|
|
54
67
|
layout((ctx) => {
|
|
55
68
|
const push = ctx.use(Breadcrumbs);
|
|
56
69
|
push({ label: "Shop", href: "/shop" });
|
|
@@ -97,7 +110,8 @@ layout(<ShopLayout />, () => [
|
|
|
97
110
|
|
|
98
111
|
## The Outlet Component
|
|
99
112
|
|
|
100
|
-
`<Outlet />` renders child content. Import from `@rangojs/router/client
|
|
113
|
+
`<Outlet />` renders child content. Import from `@rangojs/router/client`
|
|
114
|
+
(it works in server and client components):
|
|
101
115
|
|
|
102
116
|
```typescript
|
|
103
117
|
import { Outlet } from "@rangojs/router/client";
|
|
@@ -119,7 +133,9 @@ A layout's `transition()` config wraps the content that flows through `<Outlet /
|
|
|
119
133
|
|
|
120
134
|
## Named Outlets
|
|
121
135
|
|
|
122
|
-
For parallel routes, use named outlets
|
|
136
|
+
For parallel routes, use named outlets (`<Outlet name="@sidebar" />` is
|
|
137
|
+
equivalent to `<ParallelOutlet name="@sidebar" />`). Slot content is defined
|
|
138
|
+
with `parallel()` — see `/parallel`.
|
|
123
139
|
|
|
124
140
|
```typescript
|
|
125
141
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
@@ -143,40 +159,40 @@ function DashboardLayout() {
|
|
|
143
159
|
|
|
144
160
|
## Orphan Layout (inside route)
|
|
145
161
|
|
|
146
|
-
A layout
|
|
147
|
-
|
|
148
|
-
executes before its
|
|
149
|
-
|
|
150
|
-
This is the recommended way to pass handler data downward
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
skip). If an orphan layout depends on data established above its own
|
|
162
|
-
route entry, that outer segment must share a revalidation contract, or
|
|
163
|
-
the orphan must guard/reload the data independently. See `/rango` →
|
|
162
|
+
A layout declared inside a `path()` (an "orphan" layout) wraps that route's
|
|
163
|
+
content and can read what the route handler stored with `ctx.set()`. The
|
|
164
|
+
handler always executes before its orphan layouts and their parallels.
|
|
165
|
+
|
|
166
|
+
This is the recommended way to pass handler data downward. It needs no
|
|
167
|
+
revalidation setup: orphan layouts and their parallels belong to the route
|
|
168
|
+
entry, so on an action the route segment, its loaders, and these children all
|
|
169
|
+
re-run together, handler first. They can only desync if you narrow one side
|
|
170
|
+
with a predicate that returns a hard `false` — then put the same contract on
|
|
171
|
+
both (see "Revalidation Contracts").
|
|
172
|
+
|
|
173
|
+
Data from an **outer** handler or layout entry is the opposite case: outer
|
|
174
|
+
entries are skipped on actions by default. If an orphan layout depends on data
|
|
175
|
+
set above its own route entry, give that outer segment a shared revalidation
|
|
176
|
+
contract, or have the orphan load/guard the data itself. See `/rango` →
|
|
164
177
|
"Passing data down the tree" for the full safest-first ladder.
|
|
165
178
|
|
|
166
179
|
```typescript
|
|
180
|
+
import { createVar } from "@rangojs/router";
|
|
167
181
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
168
182
|
|
|
183
|
+
const CurrentProduct = createVar<Product>();
|
|
184
|
+
|
|
169
185
|
urls(({ path, layout, parallel }) => [
|
|
170
|
-
path("/product/:slug", (ctx) => {
|
|
186
|
+
path("/product/:slug", async (ctx) => {
|
|
171
187
|
const product = await fetchProduct(ctx.params.slug);
|
|
172
|
-
ctx.set(
|
|
188
|
+
ctx.set(CurrentProduct, product);
|
|
173
189
|
return <ProductPage product={product} />;
|
|
174
190
|
}, { name: "product" }, () => [
|
|
175
191
|
layout((ctx) => {
|
|
176
|
-
const product = ctx.get(
|
|
192
|
+
const product = ctx.get(CurrentProduct); // Product | undefined
|
|
177
193
|
return (
|
|
178
194
|
<div>
|
|
179
|
-
<
|
|
195
|
+
<ProductHeader name={product?.name} />
|
|
180
196
|
<Outlet />
|
|
181
197
|
<ParallelOutlet name="@related" />
|
|
182
198
|
</div>
|
|
@@ -184,7 +200,7 @@ urls(({ path, layout, parallel }) => [
|
|
|
184
200
|
}, () => [
|
|
185
201
|
parallel({
|
|
186
202
|
"@related": (ctx) => {
|
|
187
|
-
const product = ctx.get(
|
|
203
|
+
const product = ctx.get(CurrentProduct);
|
|
188
204
|
return <RelatedProducts category={product?.category} />;
|
|
189
205
|
},
|
|
190
206
|
}),
|
|
@@ -193,36 +209,65 @@ urls(({ path, layout, parallel }) => [
|
|
|
193
209
|
])
|
|
194
210
|
```
|
|
195
211
|
|
|
196
|
-
Orphan layouts can call `ctx.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
Orphan layouts can also call `ctx.set()` for their own children, but the usual
|
|
213
|
+
split is: middleware and route handlers write, orphan layouts and parallels
|
|
214
|
+
read.
|
|
215
|
+
|
|
216
|
+
Keep orphan layouts flat. A layout with no routes cannot contain another
|
|
217
|
+
`layout()`, directly or inside a `middleware(fn, () => [...])` wrapper;
|
|
218
|
+
`router.routes()` throws. Sibling layouts nest in declaration order, and a
|
|
219
|
+
sibling `middleware(fn)` runs for the whole route either way:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
path("/account", AccountPage, { name: "account" }, () => [
|
|
223
|
+
middleware(requireAuth),
|
|
224
|
+
layout(<AccountShell />), // wraps AccountNav
|
|
225
|
+
layout(<AccountNav />), // wraps AccountPage
|
|
226
|
+
]);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A `cache()` inside an orphan layout caches the whole path, like a `cache()`
|
|
230
|
+
among the path's children (`/caching`).
|
|
200
231
|
|
|
201
232
|
## Layout Revalidation
|
|
202
233
|
|
|
203
|
-
|
|
204
|
-
parent-chain segments are skipped unless a `revalidate()` opts them in.
|
|
205
|
-
(Orphan layouts inside a `path()` are the opposite: they ride along with
|
|
206
|
-
the route entry by default.) Control with `revalidate()`:
|
|
234
|
+
A standalone `layout()` renders when it is first mounted and is then kept as-is:
|
|
207
235
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
- **Navigation** between routes it already wraps does not re-render it — even
|
|
237
|
+
when the URL params change.
|
|
238
|
+
- **Actions** skip it too (the revalidation trace calls this
|
|
239
|
+
`action:parent-chain-skip`).
|
|
212
240
|
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
Orphan layouts inside a `path()` are the opposite: they belong to the route
|
|
242
|
+
entry and re-render with it (on param/search changes and on every action).
|
|
243
|
+
|
|
244
|
+
Opt a standalone layout in with `revalidate()`. Return `true` to re-render,
|
|
245
|
+
or `undefined` to defer to the default:
|
|
215
246
|
|
|
216
|
-
|
|
247
|
+
```typescript
|
|
217
248
|
import * as CartActions from "./actions/cart";
|
|
218
249
|
|
|
250
|
+
// Re-render after any cart action
|
|
219
251
|
layout(<CartLayout />, () => [
|
|
220
252
|
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
221
253
|
|
|
222
254
|
path("/cart", CartPage, { name: "cart" }),
|
|
223
255
|
])
|
|
256
|
+
|
|
257
|
+
// Re-render when a param the layout displays changes
|
|
258
|
+
layout(LocaleLayout, () => [
|
|
259
|
+
revalidate(({ currentParams, nextParams }) =>
|
|
260
|
+
currentParams.locale !== nextParams.locale || undefined,
|
|
261
|
+
),
|
|
262
|
+
|
|
263
|
+
path("/:locale/shop", ShopIndex, { name: "shop" }),
|
|
264
|
+
])
|
|
224
265
|
```
|
|
225
266
|
|
|
267
|
+
`revalidate(() => false)` on a standalone layout only restates the default.
|
|
268
|
+
The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
|
|
269
|
+
are covered in `/loader` → "`revalidate()` return shapes".
|
|
270
|
+
|
|
226
271
|
If child segments read data that was established by this layout or by a
|
|
227
272
|
route handler above them, revalidate the outer segment too. Partial
|
|
228
273
|
revalidation does not re-run non-revalidated ancestors just to rebuild
|
|
@@ -240,9 +285,11 @@ consumer segments:
|
|
|
240
285
|
|
|
241
286
|
```typescript
|
|
242
287
|
// revalidation-contracts.ts
|
|
288
|
+
import type { Revalidate } from "@rangojs/router";
|
|
243
289
|
import { addToCart } from "./actions/cart";
|
|
244
290
|
|
|
245
|
-
export const revalidateCartData = (ctx) =>
|
|
291
|
+
export const revalidateCartData: Revalidate = (ctx) =>
|
|
292
|
+
ctx.isAction(addToCart) || undefined;
|
|
246
293
|
```
|
|
247
294
|
|
|
248
295
|
```typescript
|
|
@@ -254,6 +301,10 @@ layout(<CartLayout />, () => [
|
|
|
254
301
|
]);
|
|
255
302
|
```
|
|
256
303
|
|
|
304
|
+
On the layout the contract adds the re-render the default skips after an
|
|
305
|
+
action. The route segment already re-runs after every action, so there it
|
|
306
|
+
changes nothing by default; the shared name documents the dependency.
|
|
307
|
+
|
|
257
308
|
If a segment depends on multiple upstream domains, compose multiple
|
|
258
309
|
contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
|
|
259
310
|
|
|
@@ -261,10 +312,10 @@ You can also package them as importable handoff helpers:
|
|
|
261
312
|
|
|
262
313
|
```typescript
|
|
263
314
|
// revalidation-contracts.ts
|
|
264
|
-
import { revalidate } from "@rangojs/router";
|
|
315
|
+
import { revalidate, type Revalidate } from "@rangojs/router";
|
|
265
316
|
import * as AuthActions from "./actions/auth";
|
|
266
317
|
|
|
267
|
-
export const revalidateAuthData = (ctx) =>
|
|
318
|
+
export const revalidateAuthData: Revalidate = (ctx) =>
|
|
268
319
|
ctx.isAction(AuthActions) || undefined;
|
|
269
320
|
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
270
321
|
```
|
|
@@ -281,8 +332,8 @@ layout(<ShellLayout />, () => [
|
|
|
281
332
|
## Complete Example
|
|
282
333
|
|
|
283
334
|
```typescript
|
|
284
|
-
import { urls } from "@rangojs/router";
|
|
285
|
-
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
335
|
+
import { urls, Breadcrumbs } from "@rangojs/router";
|
|
336
|
+
import { Link, Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
286
337
|
import * as CartActions from "./actions/cart";
|
|
287
338
|
|
|
288
339
|
function ShopLayout() {
|
|
@@ -290,8 +341,8 @@ function ShopLayout() {
|
|
|
290
341
|
<div className="shop">
|
|
291
342
|
<ParallelOutlet name="@promoBanner" />
|
|
292
343
|
<nav>
|
|
293
|
-
<
|
|
294
|
-
<
|
|
344
|
+
<Link to="/shop">Home</Link>
|
|
345
|
+
<Link to="/shop/cart">Cart</Link>
|
|
295
346
|
</nav>
|
|
296
347
|
<div className="content">
|
|
297
348
|
<aside>
|
|
@@ -311,9 +362,12 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
|
|
|
311
362
|
push({ label: "Shop", href: "/shop" });
|
|
312
363
|
return <ShopLayout />;
|
|
313
364
|
}, () => [
|
|
314
|
-
// Layout loaders
|
|
365
|
+
// Layout loaders: after an action, re-run only for cart actions;
|
|
366
|
+
// on navigation, undefined keeps the default
|
|
315
367
|
loader(CartLoader, () => [
|
|
316
|
-
revalidate((ctx) =>
|
|
368
|
+
revalidate((ctx) =>
|
|
369
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
370
|
+
),
|
|
317
371
|
]),
|
|
318
372
|
|
|
319
373
|
// Parallel routes
|
|
@@ -335,6 +389,8 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
|
|
|
335
389
|
Layout handlers can carry their own middleware, default parallels, and includes via `.use` so a layout becomes a self-contained unit reusable across mount sites.
|
|
336
390
|
|
|
337
391
|
```typescript
|
|
392
|
+
import { middleware, parallel, type Handler } from "@rangojs/router";
|
|
393
|
+
|
|
338
394
|
const AdminLayout: Handler = (ctx) => {
|
|
339
395
|
const user = ctx.get(CurrentUser);
|
|
340
396
|
return <Admin user={user} />;
|