@rangojs/router 0.22.1 → 0.24.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/dist/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
|
-
description: Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles,
|
|
3
|
+
description: Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, must be settled in the SSR'd document (ssr:false), or must stay out of link prefetches (prefetch:false).
|
|
4
4
|
argument-hint: "[loader]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -285,7 +285,9 @@ See `/route` for the full handler-context contract (shared with loaders) and
|
|
|
285
285
|
|
|
286
286
|
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
287
287
|
loaders called with `load(Loader, { params: { ... } })`, explicit params
|
|
288
|
-
override route-matched params.
|
|
288
|
+
override route-matched params. Explicit params are string values; a request
|
|
289
|
+
with another value or a `__proto__`, `constructor` or `prototype` key is
|
|
290
|
+
rejected (400, `load()` fails with `Invalid loader params`).
|
|
289
291
|
- `ctx.routeParams` — server-trusted route params from URL pattern matching.
|
|
290
292
|
Cannot be overridden by client-provided params.
|
|
291
293
|
|
|
@@ -333,6 +335,10 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
333
335
|
]);
|
|
334
336
|
```
|
|
335
337
|
|
|
338
|
+
In a `clientUrls()` group the same `loader(Def, () => [revalidate(fn)])` form
|
|
339
|
+
runs on the server with these arguments; the build moves the predicate out of
|
|
340
|
+
the `"use client"` module (`/client-urls` → "Revalidation runs on the server").
|
|
341
|
+
|
|
336
342
|
### `revalidate()` return shapes
|
|
337
343
|
|
|
338
344
|
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
@@ -936,6 +942,8 @@ function ProductPage() {
|
|
|
936
942
|
}
|
|
937
943
|
```
|
|
938
944
|
|
|
945
|
+
A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
|
|
946
|
+
|
|
939
947
|
## Loader Authority: notFound() and redirect()
|
|
940
948
|
|
|
941
949
|
A loader may **throw** `notFound()` and `redirect()` — data-dependent
|
|
@@ -1094,6 +1102,195 @@ The costs and constraints:
|
|
|
1094
1102
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
1095
1103
|
loader-heavy shape makes it most useful.
|
|
1096
1104
|
|
|
1105
|
+
## `prefetch: false` — Keep a Loader Out of Prefetches
|
|
1106
|
+
|
|
1107
|
+
A `<Link>` prefetch is a navigation sent early: the server renders every
|
|
1108
|
+
handler and every loader for it. With the default `"viewport"` strategy that
|
|
1109
|
+
means a grid of forty product links runs forty copies of your slowest loader
|
|
1110
|
+
for pages most visitors never open. Pass `prefetch: false` to keep one loader
|
|
1111
|
+
out:
|
|
1112
|
+
|
|
1113
|
+
```tsx
|
|
1114
|
+
import { Suspense } from "react";
|
|
1115
|
+
|
|
1116
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1117
|
+
loader(PriceLoader), // cheap: still runs in the prefetch
|
|
1118
|
+
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in a prefetch
|
|
1119
|
+
loading(<ProductSkeleton />),
|
|
1120
|
+
]),
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1124
|
+
`{ ssr: false, prefetch: false }` if you need both.)
|
|
1125
|
+
|
|
1126
|
+
What happens:
|
|
1127
|
+
|
|
1128
|
+
- A **prefetch** of the route does not run the loader and does not read its
|
|
1129
|
+
`cache()`; the payload marks it deferred. The rest of the page prefetches as
|
|
1130
|
+
usual.
|
|
1131
|
+
- Only on a route the browser does not show yet. A loader the page already
|
|
1132
|
+
holds, on a same-route navigation such as `/product/a` to `/product/b`, runs
|
|
1133
|
+
in the prefetch when it revalidates, exactly as without the flag, and the
|
|
1134
|
+
click sends no second request. The rule: a click that adopts a prefetch is
|
|
1135
|
+
never worse than the same click with no prefetch at all (one exception: a
|
|
1136
|
+
click before its prefetch has answered, under "The limits"), and content on
|
|
1137
|
+
screen is never swapped for a fallback because of the flag. For the same
|
|
1138
|
+
reason a prefetch that deferred something is used only on the page that
|
|
1139
|
+
made it: a page that already shows the segment makes its own prefetch.
|
|
1140
|
+
- A **click** that adopts that prefetch is never blocked by the loader. A
|
|
1141
|
+
deferred loader behaves like a loader that is still streaming, and where
|
|
1142
|
+
the page reads it (`useLoader(ReviewsLoader)`) React does what it does for
|
|
1143
|
+
one:
|
|
1144
|
+
- the read has its own `<Suspense>`: the page shows at once with that
|
|
1145
|
+
fallback in it. This can be a finer fallback than the same click shows
|
|
1146
|
+
with no prefetch, where the whole route is still streaming behind its
|
|
1147
|
+
`loading()`;
|
|
1148
|
+
- the nearest boundary is the route's `loading()`: that fallback shows;
|
|
1149
|
+
- no boundary at all: React holds the page being left until the value
|
|
1150
|
+
arrives.
|
|
1151
|
+
|
|
1152
|
+
Meanwhile the browser sends one second request, the fill, that runs only
|
|
1153
|
+
the deferred loader. Its value arrives in the page that is already on
|
|
1154
|
+
screen, as a streamed loader's does; nothing remounts.
|
|
1155
|
+
|
|
1156
|
+
- A **document request** (hard load), a **navigation with no prefetch to
|
|
1157
|
+
adopt**, an action, back/forward and a no-JS form post run the loader as
|
|
1158
|
+
they always did. Leaving the option out is the old behavior.
|
|
1159
|
+
|
|
1160
|
+
Read the loader behind a boundary, as you would any streamed loader:
|
|
1161
|
+
|
|
1162
|
+
```tsx
|
|
1163
|
+
"use client";
|
|
1164
|
+
function Reviews() {
|
|
1165
|
+
const { data } = useLoader(ReviewsLoader);
|
|
1166
|
+
return <ReviewList items={data} />;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// in the server component:
|
|
1170
|
+
<Suspense fallback={<ReviewsSkeleton />}>
|
|
1171
|
+
<Reviews />
|
|
1172
|
+
</Suspense>;
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
The limits:
|
|
1176
|
+
|
|
1177
|
+
- **A click before its prefetch has answered waits for it, then fetches the
|
|
1178
|
+
deferred part.** The flag is there to keep expensive or slow work out of
|
|
1179
|
+
every prefetch, so that an API that is costly to call is not called for
|
|
1180
|
+
every link a visitor might click. A click that comes while the link's
|
|
1181
|
+
prefetch is still in flight reuses that prefetch, as it does without the
|
|
1182
|
+
flag, and sends the fill when the prefetch answers. The page shows no later
|
|
1183
|
+
than with no prefetch; the deferred part shows its fallback until the fill
|
|
1184
|
+
lands, and can complete later than a click with no prefetch, by up to what
|
|
1185
|
+
was left of the prefetch. That is the price of keeping the work out of the
|
|
1186
|
+
prefetch.
|
|
1187
|
+
- **A navigation started before the fill answers cancels it.** It is
|
|
1188
|
+
cancelled when the newer navigation starts, not when that one commits. If
|
|
1189
|
+
the newer navigation then ends without replacing the page (a redirect the
|
|
1190
|
+
client refuses, an action refetch that drops its response), the deferred
|
|
1191
|
+
part keeps its fallback until the next navigation. Every other ending
|
|
1192
|
+
replaces the page anyway.
|
|
1193
|
+
- **`navigate()` resolves after the fill has answered.** The click's
|
|
1194
|
+
navigation fills the deferred part itself, so `await navigate(url)` (and a
|
|
1195
|
+
`router.push()` you await) settles a round trip after the page committed.
|
|
1196
|
+
`useNavigation()` reads `idle` from the commit, with `isStreaming` true
|
|
1197
|
+
until the fill has streamed in.
|
|
1198
|
+
- **A boundary is required for a fallback.** With no `loading()` on the entry
|
|
1199
|
+
and no `<Suspense>` around the read, nothing can show a fallback, so React
|
|
1200
|
+
holds the page being left on screen until the fill returns.
|
|
1201
|
+
`useNavigation()` reads `state: "loading"` for that time, as on any
|
|
1202
|
+
navigation that is still streaming, so a progress bar shows, and the URL
|
|
1203
|
+
hooks (`useNavigation().location`) change with the page. The address bar
|
|
1204
|
+
has already changed: it moves with the click, where a click with no
|
|
1205
|
+
prefetch moves it when its response starts. There is no definition-time
|
|
1206
|
+
error.
|
|
1207
|
+
- **`ctx.rendered()` is refused.** A loader that can be deferred throws on
|
|
1208
|
+
every request kind (a document load included, so you see it the first time
|
|
1209
|
+
you open the page in development) if it calls `await ctx.rendered()`. The
|
|
1210
|
+
fill does not re-run the entry's handler, so there is no render to wait for
|
|
1211
|
+
and no handle data to read. Remove `{ prefetch: false }` from the loader, or
|
|
1212
|
+
stop calling `ctx.rendered()`. Pushing a handle from the loader is fine.
|
|
1213
|
+
- **`ctx.set()` values from held handlers are not visible.** The fill runs the
|
|
1214
|
+
deferred work and middleware only. A handler or layout the client already
|
|
1215
|
+
holds does not run again, so a variable it set with `ctx.set()` is missing
|
|
1216
|
+
when the loader reads it with `ctx.get()`. Variables set by middleware are
|
|
1217
|
+
there, as middleware runs on every request; so are `ctx.params` and
|
|
1218
|
+
`ctx.request`.
|
|
1219
|
+
- **`redirect()`, `notFound()` and handle pushes arrive late.** They behave as
|
|
1220
|
+
for a loader that streams behind `loading()`, but they come with the fill,
|
|
1221
|
+
after the page is already on screen: a redirect becomes a replace
|
|
1222
|
+
navigation, `notFound()` renders the nearest not-found boundary, an error
|
|
1223
|
+
reaches the error boundary.
|
|
1224
|
+
- **The fill reads through the loader's own caching; a prefetch reads none
|
|
1225
|
+
of it, by intent.** A loader with `cache()` that nothing in the prefetch
|
|
1226
|
+
reads is skipped even when its entry is warm, and so is a `"use cache"`
|
|
1227
|
+
function it calls: a prefetch that looked would have to run the loader on
|
|
1228
|
+
a miss, which is the work the flag keeps out. The fill is where the loader
|
|
1229
|
+
runs. It serves a warm `cache()` entry without running the loader, runs
|
|
1230
|
+
the loader and stores on a cold one, and a `"use cache"` function inside
|
|
1231
|
+
the loader hits or stores as on any request.
|
|
1232
|
+
- **A loader something else reads with `ctx.use()` runs in the prefetch.**
|
|
1233
|
+
The reader needs the value, so the flag cannot keep the loader out. When
|
|
1234
|
+
the read is in time, the loader is an ordinary loader for that prefetch,
|
|
1235
|
+
its own `cache()` included: one run (none on a warm entry), the reader and
|
|
1236
|
+
the loader's data agree, nothing deferred, no fill for it. In time means:
|
|
1237
|
+
inside a handler that has no `loading()` (the router waits for it), or
|
|
1238
|
+
before the reader's first `await`.
|
|
1239
|
+
- **Two late readers make it run twice.** The loader runs in the prefetch for
|
|
1240
|
+
its reader and again in the fill, because by the time the read happens the
|
|
1241
|
+
loader's own segment has already been sent as deferred:
|
|
1242
|
+
|
|
1243
|
+
```tsx
|
|
1244
|
+
// 1. A handler under loading() is streamed: the router does not wait for
|
|
1245
|
+
// it, so a read after its first await comes late.
|
|
1246
|
+
path(
|
|
1247
|
+
"/product/:id",
|
|
1248
|
+
async (ctx) => {
|
|
1249
|
+
const product = await getProduct(ctx.params.id);
|
|
1250
|
+
const reviews = await ctx.use(ReviewsLoader); // late: runs it here
|
|
1251
|
+
return <Product product={product} reviews={reviews} />;
|
|
1252
|
+
},
|
|
1253
|
+
{ name: "product" },
|
|
1254
|
+
() => [
|
|
1255
|
+
loader(ReviewsLoader, { prefetch: false }), // and the fill runs it again
|
|
1256
|
+
loading(<ProductSkeleton />),
|
|
1257
|
+
],
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
// 2. A loader that is not deferred itself and reads it after an await.
|
|
1261
|
+
const SummaryLoader = createLoader(async (ctx) => {
|
|
1262
|
+
const product = await getProduct(ctx.params.id);
|
|
1263
|
+
const reviews = await ctx.use(ReviewsLoader); // late: two runs
|
|
1264
|
+
return summarize(product, reviews);
|
|
1265
|
+
});
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
Data is never missing, only fetched twice, and the page shows the fill's
|
|
1269
|
+
value. To get one run, start the read before the first `await`
|
|
1270
|
+
(`const reviews = ctx.use(ReviewsLoader)` at the top, awaited where you
|
|
1271
|
+
need it), or drop the flag from a loader the page cannot render without.
|
|
1272
|
+
|
|
1273
|
+
- **`clientUrls()` routes take it on `loader()`.** In a `clientUrls()`
|
|
1274
|
+
definition `loader(Def, { prefetch: false })` works as here: a click
|
|
1275
|
+
inside the group shows the destination at once, commits the prefetched
|
|
1276
|
+
data with the URL, and one fill runs the flagged loaders (see
|
|
1277
|
+
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1278
|
+
on a group route, from the group or from a server layout above it.
|
|
1279
|
+
- **No effect inside `intercept()`.** Its loaders accept the flag and ignore
|
|
1280
|
+
it, in `urls()` and in `clientUrls()`: a prefetch that resolves an
|
|
1281
|
+
intercept defers nothing.
|
|
1282
|
+
- **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
|
|
1283
|
+
bake lane and is served from the shell, so `prefetch: false` is ignored for
|
|
1284
|
+
it and development logs a warning. Under `cache()` and `Prerender` the flag
|
|
1285
|
+
works as above (see `/caching`, `/prerender`, `/ppr`).
|
|
1286
|
+
|
|
1287
|
+
To skip a handler along with its loaders, flag the `loading()` instead (see
|
|
1288
|
+
`/route` → "`loading()` options"). `loader(Def, { prefetch: false })` accepts
|
|
1289
|
+
only a boolean; anything else throws `loader() prefetch must be a boolean`.
|
|
1290
|
+
|
|
1291
|
+
Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
|
|
1292
|
+
and `result.readDeferred()` (see `/testing`).
|
|
1293
|
+
|
|
1097
1294
|
## Fetchable Loaders
|
|
1098
1295
|
|
|
1099
1296
|
By default, loaders only run as part of a render (document, navigation, or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: middleware
|
|
3
|
-
description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.
|
|
3
|
+
description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects, rewrites (throw rewrite() renders another route at the same URL). Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, showing a maintenance page at the visitor's URL, or deciding whether middleware wraps server actions.
|
|
4
4
|
argument-hint: [middleware-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ There are two levels of middleware with different execution scopes:
|
|
|
19
19
|
|
|
20
20
|
### Global middleware (`router.use()`)
|
|
21
21
|
|
|
22
|
-
Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
|
|
22
|
+
Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders. A request a middleware rewrites (`throw rewrite()`, below) runs the chain again for the target: once per pass.
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
25
|
const router = createRouter<AppEnv>({})
|
|
@@ -257,6 +257,8 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
257
257
|
ctx.headers; // response Headers (stub before next(), the real response after)
|
|
258
258
|
ctx.setLocationState(FlashMessage({ text: "Saved" })); // history state for the client
|
|
259
259
|
ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill)
|
|
260
|
+
ctx.rewrittenFrom; // the visitor's URL when this request was rewritten, else undefined
|
|
261
|
+
ctx.isRewrite(error); // true for a thrown rewrite() (rethrow it from a try/catch)
|
|
260
262
|
|
|
261
263
|
// Opt the current request out of PPR shell lookup/capture.
|
|
262
264
|
ctx.dynamic();
|
|
@@ -428,6 +430,185 @@ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{
|
|
|
428
430
|
|
|
429
431
|
A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
|
|
430
432
|
|
|
433
|
+
## Rewrite: another route at the same URL
|
|
434
|
+
|
|
435
|
+
`throw rewrite(target)` renders another route while the address bar keeps the
|
|
436
|
+
visitor's URL: a maintenance or lock page, a deploy migration, a URL override,
|
|
437
|
+
an A/B or geo split. A redirect changes the URL; a rewrite does not.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { getRequestContext, rewrite } from "@rangojs/router";
|
|
441
|
+
import type { Middleware } from "@rangojs/router";
|
|
442
|
+
|
|
443
|
+
export const maintenance: Middleware = async (ctx, next) => {
|
|
444
|
+
if (!locked || ctx.pathname === "/maintenance") return next();
|
|
445
|
+
// Mutations get a Response: a rewrite cannot answer a server action or a form post.
|
|
446
|
+
if (ctx.request.method !== "GET") {
|
|
447
|
+
return new Response("Maintenance", {
|
|
448
|
+
status: 503,
|
|
449
|
+
headers: { "Retry-After": "600" },
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
ctx.set(Lock, { until });
|
|
453
|
+
throw rewrite("maintenance");
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// urls.tsx — the target sets its own status
|
|
457
|
+
path("/maintenance", MaintenancePage, { name: "maintenance" }, () => [
|
|
458
|
+
middleware(async (ctx, next) => {
|
|
459
|
+
getRequestContext().setStatus(503);
|
|
460
|
+
ctx.header("Retry-After", "600");
|
|
461
|
+
await next();
|
|
462
|
+
}),
|
|
463
|
+
]);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Targets, like `ctx.reverse` with a path form:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
throw rewrite("/maintenance"); // a path (basename-prefixed like redirect())
|
|
470
|
+
throw rewrite("/maintenance?from=shop"); // a path with search
|
|
471
|
+
throw rewrite("maintenance"); // a global route name without params
|
|
472
|
+
throw rewrite("product", { slug }); // with params
|
|
473
|
+
throw rewrite("product", { slug }, { tab: "reviews" }); // and search
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**How it runs.** The throw unwinds the request to the router, which runs it
|
|
477
|
+
again as the target, from the top, as a direct visit would: global middleware
|
|
478
|
+
matched on the target's path (a `.use("/admin/*", guard)` or an inline
|
|
479
|
+
`ctx.pathname` check decides on the rendered route), the target's route
|
|
480
|
+
middleware, its render. Code after `await next()` in the pass that rewrote
|
|
481
|
+
never runs, and global middleware runs once per pass.
|
|
482
|
+
|
|
483
|
+
**Never skip a guard on a rewritten pass.** Authorization, auth and
|
|
484
|
+
rate-limit checks run on every pass, as written: the target's guards
|
|
485
|
+
deciding on the rendered route is what keeps a rewrite from becoming a way
|
|
486
|
+
around them. `if (!ctx.rewrittenFrom)` is only for side effects you want
|
|
487
|
+
once per request, such as a counter or a log line:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
export const countVisit: Middleware = async (ctx, next) => {
|
|
491
|
+
if (!ctx.rewrittenFrom) visits.increment(); // once per request
|
|
492
|
+
return next();
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Throw it before `await next()`.** Once your `next()` ran the route, the
|
|
497
|
+
page has rendered: a `throw rewrite()` after it (a "rewrite on 404" after
|
|
498
|
+
`const res = await next()`) is an error, "throw rewrite() before await
|
|
499
|
+
next()". Decide before calling `next()`. A rewrite thrown further down
|
|
500
|
+
passes through your `await next()` as usual, and in a `catch` around it you
|
|
501
|
+
may throw another rewrite in its place; after any other failure of `next()`
|
|
502
|
+
(the route may have rendered before it failed) a rewrite is an error too.
|
|
503
|
+
|
|
504
|
+
**Same request context.** Values you `ctx.set()` before the throw are
|
|
505
|
+
readable in the target's middleware, handlers and loaders; cookies, headers
|
|
506
|
+
and the status you set stay on the response; location state, theme and
|
|
507
|
+
`ctx.dynamic()` carry over. The request, URLs, params and route name are the
|
|
508
|
+
target's.
|
|
509
|
+
|
|
510
|
+
**Old and new URL.** `ctx.url`, `ctx.pathname` and `ctx.searchParams` are
|
|
511
|
+
the target's; `ctx.rewrittenFrom` is the visitor's URL (search included; the
|
|
512
|
+
first URL after several rewrites; `undefined` when nothing rewrote).
|
|
513
|
+
`usePathname()` and `useSearchParams()` show the visitor's URL in the server
|
|
514
|
+
HTML and after hydration, except in a render other visitors are served (see
|
|
515
|
+
Caches); `useParams()` and the route name are the target's. The response
|
|
516
|
+
carries `x-rango-rewrite: <target pathname>`.
|
|
517
|
+
|
|
518
|
+
**`ctx.rewrittenFrom` is request identity**, like `cookies()`: reading it
|
|
519
|
+
inside a `cache()` boundary or a `"use cache"` function throws (pass what
|
|
520
|
+
you need in from middleware), and a render other visitors may be served (a
|
|
521
|
+
PPR shell capture, a page the document cache stores) reads `undefined`, as a
|
|
522
|
+
direct visit does.
|
|
523
|
+
|
|
524
|
+
**Middleware only, by design.** `rewrite()` is a middleware tool. A route
|
|
525
|
+
handler or a loader (any loader, `ssr: false` included) cannot rewrite: its
|
|
526
|
+
throw is an error whose message points you to route middleware. A rewrite
|
|
527
|
+
that depends on data goes in the route's middleware, reading the data
|
|
528
|
+
through a cached function the loader shares, so the lookup runs once:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
532
|
+
middleware(async (ctx, next) => {
|
|
533
|
+
const product = await getProduct(ctx.params.slug); // cached, shared with the loader
|
|
534
|
+
if (product.discontinued) {
|
|
535
|
+
throw rewrite("discontinued", { slug: ctx.params.slug });
|
|
536
|
+
}
|
|
537
|
+
await next();
|
|
538
|
+
}),
|
|
539
|
+
loader(productLoader), // calls getProduct(slug): a cache hit
|
|
540
|
+
]);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Throw it, don't return it.** A returned rewrite is an error. Where a
|
|
544
|
+
rewrite cannot be honored it is an error that names the rule: a route
|
|
545
|
+
handler or a loader (see above), a server action (use `redirect()`),
|
|
546
|
+
intercept middleware, a loader fetch (`useFetchLoader`, `load()`: it
|
|
547
|
+
rejects with the rule) or its middleware, and middleware handling a non-GET
|
|
548
|
+
request (a JS action or a no-JS form post: return a `Response`).
|
|
549
|
+
|
|
550
|
+
**try/catch around next().** A thrown rewrite passes through your
|
|
551
|
+
middleware's `catch`. Rethrow it:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
export const errorPage: Middleware = async (ctx, next) => {
|
|
555
|
+
try {
|
|
556
|
+
return await next();
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (ctx.isRewrite(error)) throw error;
|
|
559
|
+
return new Response("Something went wrong", { status: 500 });
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Loops.** More than 5 rewrites in one request, or a rewrite to the URL the
|
|
565
|
+
request is on, answers 508 and reports a `RewriteLoopError` (its message has
|
|
566
|
+
the trail, `/a -> /b -> /a`) through `onError` with phase `"routing"`.
|
|
567
|
+
|
|
568
|
+
**Caches.**
|
|
569
|
+
|
|
570
|
+
- The document cache keys a rewritten request by the target's URL, as a
|
|
571
|
+
direct visit. Register the rewriting middleware **before**
|
|
572
|
+
`createDocumentCacheMiddleware()`: a cache hit for the visitor's URL would
|
|
573
|
+
be served before your middleware runs. A page the document cache stores,
|
|
574
|
+
and a PPR shell of the target, are rendered as a direct visit to the
|
|
575
|
+
target: they carry the target's pathname and `ctx.rewrittenFrom` is
|
|
576
|
+
`undefined` there, so a component that renders `usePathname()` can
|
|
577
|
+
mismatch on hydration at the visitor's URL when such a page is served to
|
|
578
|
+
a rewritten request.
|
|
579
|
+
- The client's prefetch and history caches keep a rewritten page under the
|
|
580
|
+
visitor's URL, and nothing tells them about the rewrite: a page prefetched
|
|
581
|
+
during a lock shows the lock page for up to `prefetchCacheTTL` after the
|
|
582
|
+
lock lifts. `invalidateClientCache()` drops a client's caches, for the
|
|
583
|
+
client that receives the response; mark the clients that saw the lock and
|
|
584
|
+
call it on their first response after it lifts:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { cookies, invalidateClientCache, rewrite } from "@rangojs/router";
|
|
588
|
+
|
|
589
|
+
export const maintenance: Middleware = async (ctx, next) => {
|
|
590
|
+
if (!locked) {
|
|
591
|
+
if (cookies().get("saw-lock")) {
|
|
592
|
+
cookies().delete("saw-lock");
|
|
593
|
+
invalidateClientCache();
|
|
594
|
+
}
|
|
595
|
+
return next();
|
|
596
|
+
}
|
|
597
|
+
cookies().set("saw-lock", "1", { path: "/" });
|
|
598
|
+
// ...the lock as above
|
|
599
|
+
throw rewrite("maintenance");
|
|
600
|
+
};
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Testing.** `runMiddleware` reports a thrown rewrite as `result.rewrite`
|
|
604
|
+
(`{ pathname, search, routeName? }`). `serveShellRequest` takes a rewritten
|
|
605
|
+
page request through the full production pipeline (document or `partial`
|
|
606
|
+
navigation): the target's middleware, render, status and `x-rango-rewrite`.
|
|
607
|
+
`dispatch` re-dispatches too, for response routes only: it matches the
|
|
608
|
+
visitor's URL first and throws on a component (RSC) route.
|
|
609
|
+
`renderRoute(..., { request: "/shop", rewrite: "/maintenance" })` renders
|
|
610
|
+
the target's route at the visitor's URL. See `/testing`.
|
|
611
|
+
|
|
431
612
|
## Authentication Middleware
|
|
432
613
|
|
|
433
614
|
```typescript
|
|
@@ -850,8 +850,8 @@ For **dashboard / admin / settings-shaped sections** — high navigation
|
|
|
850
850
|
frequency inside one layout, mostly tab/param/filter switches — also consider
|
|
851
851
|
porting that route group to `clientUrls()` (`/client-urls`): the definition
|
|
852
852
|
matches in the browser (instant optimistic pending, no server round-trip to
|
|
853
|
-
start a transition) and
|
|
854
|
-
switches that don't invalidate it, which is the fastest transition shape Rango
|
|
853
|
+
start a transition) and per-loader `revalidate()` predicates (run on the
|
|
854
|
+
server) hold data across switches that don't invalidate it, which is the fastest transition shape Rango
|
|
855
855
|
has. Server-component routes and `clientUrls()` groups compose in one tree.
|
|
856
856
|
|
|
857
857
|
## 7. Server Actions
|
|
@@ -84,21 +84,24 @@ per app (both compose in one `urls()` tree via `include()`):
|
|
|
84
84
|
| component (client, hooks) | stays a client component — no conversion |
|
|
85
85
|
| `loader` (throws `redirect`/404) | `createLoader()` — thrown `redirect()`/`notFound()` kept |
|
|
86
86
|
| `useLoaderData()` | `useLoader(Loader)` at the read site, under `<Suspense>` |
|
|
87
|
-
| `shouldRevalidate` (runs client) | `revalidate()` predicate —
|
|
87
|
+
| `shouldRevalidate` (runs client) | `revalidate()` predicate — runs on the server |
|
|
88
88
|
| `meta({ data })` | `ctx.use(Meta)` push from the loader body |
|
|
89
89
|
| `defer` / `Await` | loaders stream; `<Suspense>` above each read |
|
|
90
90
|
|
|
91
|
-
Note the `shouldRevalidate` row:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
Note the `shouldRevalidate` row: RR runs it in the browser, but a
|
|
92
|
+
`clientUrls()` loader's predicate runs on the server like a server-tree
|
|
93
|
+
`revalidate()`, with the same arguments (`actionResult` and `formData`
|
|
94
|
+
included). The build moves it out of the `"use client"` module, so it may
|
|
95
|
+
reference imports and globals only: move a predicate that captures
|
|
96
|
+
module-local values into its own module and import it. A group ported this
|
|
97
|
+
way can still be re-migrated to server handlers later, route by route.
|
|
95
98
|
|
|
96
99
|
Start with server handlers; reach for `clientUrls()` when a route group's
|
|
97
100
|
conversion cost is dominated by rewriting interactive components rather than
|
|
98
101
|
by moving data fetching — or when the group is a **high-navigation-speed
|
|
99
102
|
surface** (dashboard, admin panel, settings): browser-local matching gives
|
|
100
|
-
instant optimistic pending, and
|
|
101
|
-
tab/param switches, so transitions are the fastest Rango offers. See
|
|
103
|
+
instant optimistic pending, and per-loader `revalidate()` predicates (run on
|
|
104
|
+
the server) hold data across tab/param switches, so transitions are the fastest Rango offers. See
|
|
102
105
|
`/client-urls`.
|
|
103
106
|
|
|
104
107
|
## Migration Strategy
|
|
@@ -55,7 +55,7 @@ Use it only when you need capabilities beyond what the handler provides:
|
|
|
55
55
|
If the React Router loader just fetches data for its page component AND the
|
|
56
56
|
component can become a server component, merge it into the handler. If the
|
|
57
57
|
component stays a client component, port the whole group with `clientUrls()`
|
|
58
|
-
instead — loader, `useLoader` read, and
|
|
58
|
+
instead — loader, `useLoader` read, and per-loader `revalidate()` keep the RR
|
|
59
59
|
route-module shape (see the "Two target shapes" section in the main skill and
|
|
60
60
|
`/client-urls`). See `/loader` for when the live data layer is useful.
|
|
61
61
|
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -149,6 +149,15 @@ material is shared per host+URL and request partition. `cache(false)`, or a
|
|
|
149
149
|
`condition()` that returns false, on a ppr route now renders the request like a
|
|
150
150
|
cache miss (no shell).
|
|
151
151
|
|
|
152
|
+
### A request middleware rewrote
|
|
153
|
+
|
|
154
|
+
A request middleware rewrote (`throw rewrite()`, `/middleware`) to a `ppr`
|
|
155
|
+
route is served the target's shell under the target's key. The capture
|
|
156
|
+
renders the target as a direct visit: `ctx.rewrittenFrom` reads `undefined`
|
|
157
|
+
there and the payload carries the target's pathname, so the shell is the
|
|
158
|
+
same for direct and rewritten visitors (a component that renders
|
|
159
|
+
`usePathname()` shows the target's URL to a rewritten visitor on a HIT).
|
|
160
|
+
|
|
152
161
|
### Request-partitioned shells
|
|
153
162
|
|
|
154
163
|
A route whose `cache()` record is partitioned by the request partitions its
|
|
@@ -1069,6 +1078,19 @@ Four hard edges (each e2e/unit-pinned):
|
|
|
1069
1078
|
must be fresh on every serve, it belongs on the live lane (no `ssr: false`) or
|
|
1070
1079
|
in a nested promise.
|
|
1071
1080
|
|
|
1081
|
+
### `prefetch: false` on a `ppr` route
|
|
1082
|
+
|
|
1083
|
+
A `ppr` route's handler output is stored, so a `loading()` flagged with
|
|
1084
|
+
`prefetch: false` does not skip the handler in a `<Link>` prefetch: the shell
|
|
1085
|
+
and its partial replay are served as usual and only the live loaders behind
|
|
1086
|
+
the fallback are deferred. A live loader registered with `{ prefetch: false }` is
|
|
1087
|
+
skipped the same way, and the click that adopts the prefetch sends one fill
|
|
1088
|
+
request for it. An `ssr: false` loader is the bake lane and is served from the
|
|
1089
|
+
shell, so on a `ppr` route `prefetch: false` is ignored for it, and
|
|
1090
|
+
development logs a warning that the loader "is registered with ssr: false
|
|
1091
|
+
and prefetch: false on a ppr route"; drop one of the two options. See
|
|
1092
|
+
`/loader` → "`prefetch: false`".
|
|
1093
|
+
|
|
1072
1094
|
### The layout-with-loaders playbook (the storefront case)
|
|
1073
1095
|
|
|
1074
1096
|
The most common real-app shape: an app-wide layout registers per-user loaders
|
|
@@ -666,6 +666,16 @@ build-time shell capture runs it and bakes its settled data into the shell,
|
|
|
666
666
|
and a shell HIT runs it only when its return carries promises (`/ppr` → "The
|
|
667
667
|
bake lane"). The Flight payload collection itself still skips loaders.
|
|
668
668
|
|
|
669
|
+
### `prefetch: false` skips only the loaders
|
|
670
|
+
|
|
671
|
+
A `Prerender` handler's output is stored, so a `loading()` flagged with
|
|
672
|
+
`prefetch: false` on the route does not skip the handler in a `<Link>` prefetch: the
|
|
673
|
+
stored output is served as usual, and only the loaders behind the fallback are
|
|
674
|
+
deferred (a loader registered with `{ prefetch: false }` likewise). The click
|
|
675
|
+
that adopts the prefetch shows the fallback and sends one fill request for
|
|
676
|
+
them. This holds in development, where there is no build artifact, as in
|
|
677
|
+
production. See `/loader` → "`prefetch: false`".
|
|
678
|
+
|
|
669
679
|
### Build-time handle data is frozen
|
|
670
680
|
|
|
671
681
|
Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
|
|
@@ -1283,8 +1293,8 @@ fresh.
|
|
|
1283
1293
|
A warm replaces the entry where it runs, and on a shared store that is the
|
|
1284
1294
|
entry other locations read on their next miss. It does not reach a copy
|
|
1285
1295
|
another location already holds: on `CFCacheStore`, each colo keeps serving its
|
|
1286
|
-
own Cache API copy until it expires
|
|
1287
|
-
60 seconds never reaches KV at all.
|
|
1296
|
+
own Cache API copy until it expires or a tag invalidation rejects it, and an
|
|
1297
|
+
entry whose `ttl + swr` is under 60 seconds never reaches KV at all.
|
|
1288
1298
|
|
|
1289
1299
|
So after a deploy (new keys: no location holds a copy) a warm alone makes
|
|
1290
1300
|
the route ready everywhere. For a content change, invalidate first, then
|
|
@@ -1306,12 +1316,16 @@ path.json("/hooks/product-changed", async (ctx) => {
|
|
|
1306
1316
|
|
|
1307
1317
|
The invalidation makes every location's copy unservable, so no visitor is
|
|
1308
1318
|
served the old content, and the warm writes the new entry where it runs and
|
|
1309
|
-
to the shared store.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1319
|
+
to the shared store. Every other location reads that entry on its next
|
|
1320
|
+
request and renders nothing: a location with no copy of its own, and, on
|
|
1321
|
+
`CFCacheStore` with `kvFallThrough: true` (default false), a colo that was
|
|
1322
|
+
still holding its own (now invalidated) copy, which reads KV as if it held
|
|
1323
|
+
none. Without the option such a colo renders once for itself, unless the store
|
|
1324
|
+
uses `tagPurge`, which evicts those copies. With the option, a colo renders
|
|
1325
|
+
for itself only while KV still hands it the entry from before the invalidation,
|
|
1326
|
+
which it refuses: Cloudflare documents up to 60
|
|
1327
|
+
seconds for a KV write to spread. Await `updateTag()` before the warm: a
|
|
1328
|
+
write that started before an invalidation of one of its tags is refused.
|
|
1315
1329
|
|
|
1316
1330
|
### Limits worth knowing
|
|
1317
1331
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -168,13 +168,16 @@ stated, greppable contract.
|
|
|
168
168
|
only applies when the enclosing cache recomputes; put a value in a loader if it
|
|
169
169
|
must be fresher. See `/cache-guide` → "Combining Both".
|
|
170
170
|
- **A predicate runs where its effect lands.** A server-tree `revalidate()`
|
|
171
|
-
|
|
171
|
+
and a `clientUrls()` loader's `revalidate()` decide what the server re-sends,
|
|
172
|
+
so they run on the server (the build hoists a group's predicate out of its
|
|
173
|
+
`"use client"` module and passes it the `urls()` arguments: `/client-urls`);
|
|
174
|
+
an `intercept({
|
|
172
175
|
when })` selector decides what the server renders, so it runs on the server;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
`transition({ when })` decides a browser commit, so it runs in the browser.
|
|
177
|
+
A `transition({ when })` predicate sees URLs, params, route names, history
|
|
178
|
+
state and action data, never `get()`, `env` or handler state; in a server
|
|
179
|
+
`urls()` it is written inline (the build hoists it into a client module) or
|
|
180
|
+
imported from a `"use client"` module (`/view-transitions`).
|
|
178
181
|
- **Vocabulary.** `revalidate()` keeps React Router's `shouldRevalidate`
|
|
179
182
|
dialect (`currentUrl`/`nextUrl`, `currentParams`/`nextParams`). Navigation
|
|
180
183
|
selectors (`intercept({ when })`, `transition({ when })`) use `from`/`to`
|
|
@@ -270,6 +273,9 @@ the `path()` re-run; layouts and parallels above the route are skipped.
|
|
|
270
273
|
revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
|
|
271
274
|
```
|
|
272
275
|
|
|
276
|
+
The same `actionResult` idiom works in a `clientUrls()` group's loader
|
|
277
|
+
`revalidate()`, which runs on the server with these arguments.
|
|
278
|
+
|
|
273
279
|
**The source is the source of truth.** Structure, types, and update policy are
|
|
274
280
|
visible and local in the tree — read top-down, no hidden global model to hold in
|
|
275
281
|
your head. A snippet earns its place only if, from the code alone, you can answer:
|
|
@@ -305,7 +311,7 @@ Grouped by concern — read when you need to…
|
|
|
305
311
|
| `/layout` | Layouts that wrap child routes |
|
|
306
312
|
| `/parallel` | Multi-column layouts and sidebars |
|
|
307
313
|
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
308
|
-
| `/middleware` | Request processing
|
|
314
|
+
| `/middleware` | Request processing, authentication, `throw rewrite()` (same-URL rewrites) |
|
|
309
315
|
| `/host-router` | Multi-app host routing with domain/subdomain patterns |
|
|
310
316
|
| `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
|
|
311
317
|
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|