@rangojs/router 0.24.4 → 0.25.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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -214,14 +214,16 @@ export const shopPatterns = urls(({ path, include }) => [
|
|
|
214
214
|
export default shopPatterns; // async include() resolves this
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
217
|
+
**Default to the eager (sync) form.** Reach for the async form when a group is
|
|
218
|
+
large and rarely visited, and is a natural, independently-loadable unit (a big
|
|
219
|
+
localized section, a heavy admin area, an API surface with heavy handlers): it
|
|
220
|
+
trims the eagerly-parsed entry bundle and keeps that subgraph off the cold-start
|
|
221
|
+
path, at the cost of the group's first request paying its chunk import (see the
|
|
222
|
+
measurements below). A thin group, or one that shares most of its module graph
|
|
223
|
+
with the entry, gains little from splitting, since the bundler keeps shared
|
|
224
|
+
modules common regardless. Both match identically at runtime — only the module's
|
|
225
|
+
runtime evaluation timing differs. `/app-structure` shows both in whole-app
|
|
226
|
+
layouts.
|
|
225
227
|
|
|
226
228
|
What you do NOT lose by splitting: build-time discovery `await`s the provider, so
|
|
227
229
|
`href()`, `ctx.reverse()`, generated route types, and prerender still see every route
|
|
@@ -51,14 +51,14 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
51
51
|
|
|
52
52
|
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
53
|
|
|
54
|
-
| Mount site | Allowed item types
|
|
55
|
-
| ------------------------------------------------- |
|
|
56
|
-
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition`
|
|
57
|
-
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include`
|
|
58
|
-
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition`
|
|
59
|
-
| `intercept()` | `
|
|
60
|
-
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache`
|
|
61
|
-
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache`
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `middleware` goes on the target route (it runs for the modal too), `revalidate` on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
|
+
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
|
|
62
62
|
|
|
63
63
|
`route` is the item type a `path()` call produces.
|
|
64
64
|
|
|
@@ -292,7 +292,7 @@ AdminLayout.use = () => [
|
|
|
292
292
|
|
|
293
293
|
### Intercepts (`intercept()`)
|
|
294
294
|
|
|
295
|
-
Intercept handlers can carry their own
|
|
295
|
+
Intercept handlers can carry their own loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. `middleware()`, boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)); the target route's middleware runs for the modal.
|
|
296
296
|
|
|
297
297
|
```typescript
|
|
298
298
|
const QuickViewModal: Handler = async (ctx) => {
|
|
@@ -392,6 +392,44 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
392
392
|
- **Validation runs at registration / first match**, not at handler definition. A handler doesn't know its mount site at definition time — the same handler used in a `path()` and an `intercept()` is validated against each mount's allowed-types set when registered.
|
|
393
393
|
- **No silent shadowing.** If a disallowed item slips through (e.g., a layout factory returning `cache()` from a slot's `handler.use`), the runtime throws with the offending type and mount site named.
|
|
394
394
|
|
|
395
|
+
## In `clientUrls()`: `Component.use`
|
|
396
|
+
|
|
397
|
+
A client route group (`/client-urls`) takes the same pattern for its
|
|
398
|
+
components: `Dashboard.use = () => [...]` on a page, layout or intercept
|
|
399
|
+
component mounted by the group's `path()`, `layout()` or `intercept()`. Build
|
|
400
|
+
the items with the helpers `@rangojs/router/client` exports (`loader`,
|
|
401
|
+
`loading`, `revalidate`, `transition`); the `@rangojs/router` ones are the
|
|
402
|
+
server DSL's and throw in a client module.
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
"use client";
|
|
406
|
+
import { loader, loading, revalidate } from "@rangojs/router/client";
|
|
407
|
+
|
|
408
|
+
Dashboard.use = () => [
|
|
409
|
+
loader(StatsLoader, () => [
|
|
410
|
+
revalidate(({ isAction }) => isAction(StatsActions)),
|
|
411
|
+
]),
|
|
412
|
+
loading(<DashboardSkeleton />),
|
|
413
|
+
];
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The composition rules are this skill's: the component's items first, the
|
|
417
|
+
mount site's explicit items second, `loading()` and `transition()` last-wins,
|
|
418
|
+
loaders accumulate. What differs:
|
|
419
|
+
|
|
420
|
+
| Mount site in `clientUrls()` | The component's `.use` may return |
|
|
421
|
+
| ---------------------------- | ------------------------------------------ |
|
|
422
|
+
| `path()` | `loader`, `loading`, `transition` |
|
|
423
|
+
| `layout()` | `loader`, `loading` |
|
|
424
|
+
| `intercept()` | `loader` (without `ssr: false`), `loading` |
|
|
425
|
+
|
|
426
|
+
- No `route`, `layout`, `intercept` or `include` items: a group's routes and
|
|
427
|
+
its generated types come from the builder's own items.
|
|
428
|
+
- A `revalidate()` predicate still runs on the server: the build hoists it
|
|
429
|
+
out of the component's module (see `/client-urls`, "Revalidation runs on
|
|
430
|
+
the server").
|
|
431
|
+
- A `createLoader()` definition's `.use` is not read inside a group yet.
|
|
432
|
+
|
|
395
433
|
## Cross-references
|
|
396
434
|
|
|
397
435
|
- `skills/route` — `path()` mount site basics
|
|
@@ -400,3 +438,4 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
400
438
|
- `skills/intercept` — intercept mount site basics
|
|
401
439
|
- `skills/loader` — defining `createLoader` and reading via `ctx.use()`
|
|
402
440
|
- `skills/middleware` — middleware semantics and ordering
|
|
441
|
+
- `skills/client-urls` — `Component.use` in a client route group
|
|
@@ -39,6 +39,107 @@ the capture, so its pushes always apply client-side; with `ssr: false` they
|
|
|
39
39
|
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
40
40
|
re-renders on every handle update, late loader pushes included.
|
|
41
41
|
|
|
42
|
+
### When handle values change (by design)
|
|
43
|
+
|
|
44
|
+
Handle values are reactive and arrive on their own channel, by design. A value
|
|
45
|
+
belongs to the segment that pushed it and reaches `useHandle` when that push
|
|
46
|
+
settles. The page never waits for a handle, and a handle never shows the next
|
|
47
|
+
page's values over the page being left:
|
|
48
|
+
|
|
49
|
+
| Case | What `useHandle` returns |
|
|
50
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. |
|
|
52
|
+
| Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
|
|
53
|
+
| Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
|
|
54
|
+
| `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
|
|
55
|
+
|
|
56
|
+
You never see a pending value, a blank, or the destination's title over the old
|
|
57
|
+
page. A crumb whose `content` is a promise read with `use()` in `<Suspense>`
|
|
58
|
+
never holds the page either; only that boundary waits for it.
|
|
59
|
+
|
|
60
|
+
**What this means for effects.** An effect keyed only on the URL
|
|
61
|
+
(`usePathname()`) can run before the destination's values arrive and read the
|
|
62
|
+
previous page's value. That is the contract, not a bug. When a reader must tie a
|
|
63
|
+
handle value to a page (an analytics `page_view`), put the page's path in the
|
|
64
|
+
handle value and key the effect on the value, deduped by path:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// handles/page-view.ts
|
|
68
|
+
import { createHandle } from "@rangojs/router";
|
|
69
|
+
|
|
70
|
+
export interface PageView {
|
|
71
|
+
path: string;
|
|
72
|
+
title?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Collect the item, not the array of segment arrays.
|
|
76
|
+
export const PageView = createHandle<PageView, PageView | undefined>(
|
|
77
|
+
(segments) => segments.flat().at(-1),
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// urls: a root slot that re-runs on every navigation
|
|
83
|
+
parallel(
|
|
84
|
+
{
|
|
85
|
+
"@pageView": (ctx) => {
|
|
86
|
+
ctx.use(PageView)({ path: ctx.pathname + ctx.url.search });
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
() => [revalidate(true)],
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
"use client";
|
|
96
|
+
import { useEffect, useRef } from "react";
|
|
97
|
+
import { useHandle } from "@rangojs/router/client";
|
|
98
|
+
import { PageView } from "./handles/page-view";
|
|
99
|
+
|
|
100
|
+
export function PageViews() {
|
|
101
|
+
const pv = useHandle(PageView);
|
|
102
|
+
// Seeded with the landing path: the first page_view is sent elsewhere (a
|
|
103
|
+
// bootstrap script). Start from `null` to send it from here instead. The
|
|
104
|
+
// component also renders on the server, where there is no `window`.
|
|
105
|
+
const last = useRef(
|
|
106
|
+
typeof window === "undefined"
|
|
107
|
+
? null
|
|
108
|
+
: window.location.pathname + window.location.search,
|
|
109
|
+
);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!pv || pv.path === last.current) return;
|
|
112
|
+
last.current = pv.path;
|
|
113
|
+
gtag("event", "page_view", { page_path: pv.path, page_title: pv.title });
|
|
114
|
+
}, [pv]);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The effect fires once per page, because the value carries its own path: the
|
|
120
|
+
previous page's value has the previous path and is skipped, and the destination's
|
|
121
|
+
fires when it arrives. Trade-offs:
|
|
122
|
+
|
|
123
|
+
- Dedupe is by path. Include `ctx.url.search` in `path` if search-only
|
|
124
|
+
navigations count; a same-URL refresh or an action's revalidation re-delivers
|
|
125
|
+
an equal value and sends nothing. Under dev StrictMode the mount effect runs
|
|
126
|
+
twice; the path check covers it.
|
|
127
|
+
- Select or collect the item, not an array: `useHandle` keeps its reference
|
|
128
|
+
while the value is equal one level deep, and an array of objects is rebuilt on
|
|
129
|
+
every delivery (an action's included). With the default collector, use
|
|
130
|
+
`useHandle(PageView, (s) => s.at(-1)?.at(-1))`.
|
|
131
|
+
- Async values arrive later. With `push(promise)` the page_view fires when the
|
|
132
|
+
promise resolves, with the resolved value; read the title from the value, not
|
|
133
|
+
from `document.title`.
|
|
134
|
+
- A page that pushes nothing under its own `PageView` keeps the previous value
|
|
135
|
+
and fires nothing for itself; the root slot above stamps the path on every
|
|
136
|
+
navigation so every route counts.
|
|
137
|
+
- Back/Forward fires (the entry's path differs from the current one).
|
|
138
|
+
|
|
139
|
+
A server component that pushes after an `await` throws `LateHandlePushError`:
|
|
140
|
+
reserve the slot in the handler with `ctx.use(Handle).defer()` and resolve it
|
|
141
|
+
from the component (`/breadcrumbs`, "Deferred content").
|
|
142
|
+
|
|
42
143
|
On a document load `useHandle` hydrates with the handle data its HTML was
|
|
43
144
|
rendered from, wherever the reader sits; late loader pushes reach every reader
|
|
44
145
|
once the document has finished streaming and every boundary has hydrated (see
|
|
@@ -107,6 +208,9 @@ function AddToCartButton({ productId }: { productId: string }) {
|
|
|
107
208
|
// payload: unknown[] | FormData | null (the call's arguments)
|
|
108
209
|
// error: unknown | null (whatever the action threw)
|
|
109
210
|
// result: unknown | null (return value)
|
|
211
|
+
// revalidationError: unknown | null (a loader that threw while it reran
|
|
212
|
+
// after the action; the action succeeded and the loader threw at its
|
|
213
|
+
// read: /client-urls -> "A loader that fails on its rerun")
|
|
110
214
|
|
|
111
215
|
return (
|
|
112
216
|
// .bind keeps the form progressively enhanced; useAction still matches it
|
|
@@ -35,15 +35,23 @@ on screen:
|
|
|
35
35
|
|
|
36
36
|
| What you want to show | Read |
|
|
37
37
|
| ---------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
38
|
-
| a request is out, nothing has arrived | `
|
|
38
|
+
| a request is out, nothing has arrived | `pendingUrl` is the URL being fetched; `null` once the response arrives |
|
|
39
|
+
| the new page is not on screen yet | `state === "loading"`; `"idle"` commits together with the new page and its URL |
|
|
39
40
|
| the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
|
|
40
41
|
| which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
|
|
41
42
|
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
`pendingUrl` covers the network wait. Once the response has arrived a loader
|
|
44
|
+
may still be streaming, and React keeps the page being left on screen where a
|
|
45
|
+
navigation holds its content (a same-route change, a `transition()` route,
|
|
46
|
+
with or without a view transition). `state` stays `"loading"` through that
|
|
47
|
+
part: for a navigation on its own, `"idle"` commits in the same commit as the
|
|
48
|
+
destination's `useSearchParams()`, `usePathname()` and `useParams()`, so a
|
|
49
|
+
component that reads both does not see the navigation finished on the URL it
|
|
50
|
+
is leaving. A server action, or another navigation started and aborted,
|
|
51
|
+
while React holds that page can still bring `"idle"` with the URL being left.
|
|
52
|
+
`isStreaming` can outlast that commit (a loader still streaming below a
|
|
53
|
+
boundary that already showed), so use it for a progress indicator that should
|
|
54
|
+
last until the new page is complete.
|
|
47
55
|
|
|
48
56
|
On Back/Forward the address bar changes at once, before any of this: during
|
|
49
57
|
the wait `window.location` is the entry being fetched, the same URL as
|
|
@@ -51,9 +59,9 @@ the wait `window.location` is the entry being fetched, the same URL as
|
|
|
51
59
|
from the client's history cache restores its page in one commit and reports
|
|
52
60
|
no pending state.
|
|
53
61
|
|
|
54
|
-
`location` moves
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
`location` moves in that commit too. A component that first mounts while
|
|
63
|
+
React holds the page being left reads the destination's `location` and URL
|
|
64
|
+
hooks (issue #1046).
|
|
57
65
|
|
|
58
66
|
### useRouter()
|
|
59
67
|
|
|
@@ -103,7 +111,10 @@ structured-clone-safe data; see [`./state.md`](./state.md)).
|
|
|
103
111
|
Target resolution: a path starting with `/` is app-absolute and gets the
|
|
104
112
|
router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
|
|
105
113
|
against the current `include()` mount, so `router.push("cart")` inside
|
|
106
|
-
`include("/shop", ...)` navigates to `/shop/cart`.
|
|
114
|
+
`include("/shop", ...)` navigates to `/shop/cart`. A parameterized mount is
|
|
115
|
+
filled from the current route params: inside `include("/:accountId", ...)` at
|
|
116
|
+
`/acme/board`, `router.push("cart")` navigates to `/acme/cart`; a missing
|
|
117
|
+
mount param rejects the returned promise.
|
|
107
118
|
|
|
108
119
|
#### Skipping revalidation
|
|
109
120
|
|
|
@@ -176,6 +187,9 @@ const segments = useSegments();
|
|
|
176
187
|
const part = segments.path[depth];
|
|
177
188
|
```
|
|
178
189
|
|
|
190
|
+
Like `useHandle`, it commits with the page: while React holds the page being
|
|
191
|
+
left, `useSegments()` keeps that page's path and segments.
|
|
192
|
+
|
|
179
193
|
### useLinkStatus()
|
|
180
194
|
|
|
181
195
|
Track pending state inside a Link component. `pending` is `true` while a
|
package/skills/hooks/urls.md
CHANGED
|
@@ -172,7 +172,7 @@ function ShopNav() {
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
175
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. Under a parameterized mount (`include("/:accountId", ...)`) it fills the mount's params from the current route params (`/acme/cart` at `/acme/board`); a missing one makes the returned function throw `Missing param`. The function is referentially stable while the mount's own param values stay the same, so it is safe as a hook dependency or memoized prop.
|
|
176
176
|
|
|
177
177
|
### useMount()
|
|
178
178
|
|
package/skills/i18n/SKILL.md
CHANGED
|
@@ -162,7 +162,8 @@ ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unpre
|
|
|
162
162
|
`ctx.reverse()` auto-fills missing params from the current request, so the
|
|
163
163
|
last call returns `/en/c/breads` when the current URL is `/en/...`. That is
|
|
164
164
|
what you want for in-locale links; to leave the locale, pass it explicitly
|
|
165
|
-
(`{ locale: "fr" }`, or `undefined`/`""` for the bare default
|
|
165
|
+
(`{ locale: "fr" }`, or `undefined`/`""` for the bare default; under a
|
|
166
|
+
required `/:locale`, `undefined` throws `Missing param "locale"`). Middleware and
|
|
166
167
|
response routes are the exception: their `ctx.reverse` fills nothing from the
|
|
167
168
|
request, so pass `locale` there explicitly.
|
|
168
169
|
|
|
@@ -92,35 +92,39 @@ Only the first `layout()` in an intercept's `use()` is applied. It is modal
|
|
|
92
92
|
chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
|
|
93
93
|
Attach loaders and the rest to the intercept itself.
|
|
94
94
|
|
|
95
|
-
## Intercept
|
|
95
|
+
## Middleware and an Intercept
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
An intercept has no middleware of its own. The target route's middleware runs
|
|
98
|
+
for the modal on every request that renders it, a `cache()` hit and a
|
|
99
|
+
`Prerender()` target included. The chain for an intercept request is:
|
|
98
100
|
|
|
99
101
|
```
|
|
100
|
-
global mw (router.use) -> route mw (
|
|
102
|
+
global mw (router.use) -> route mw (the target route's middleware()) -> intercept loaders + handler
|
|
101
103
|
```
|
|
102
104
|
|
|
103
105
|
The intercept's loaders are started before its handler is called, so they run
|
|
104
|
-
in parallel with it. If
|
|
105
|
-
redirect), the
|
|
106
|
+
in parallel with it. If route middleware returns a `Response` (e.g. a
|
|
107
|
+
redirect), the request stops there, the modal with it.
|
|
106
108
|
|
|
107
109
|
```typescript
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
loader(ProductLoader),
|
|
119
|
-
]
|
|
120
|
-
)
|
|
110
|
+
path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
111
|
+
middleware(async (ctx, next) => {
|
|
112
|
+
// Runs for the full page and for the modal
|
|
113
|
+
ctx.set(Viewer, await getViewer(ctx.request));
|
|
114
|
+
await next();
|
|
115
|
+
}),
|
|
116
|
+
]),
|
|
117
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
118
|
+
loader(ProductLoader), // ctx.get(Viewer) is set here
|
|
119
|
+
]),
|
|
121
120
|
```
|
|
122
121
|
|
|
123
|
-
|
|
122
|
+
`middleware()` in an intercept's `use()`, or in its handler's `.use`, throws
|
|
123
|
+
when the routes are defined: move it to the route's `middleware()`, or do the
|
|
124
|
+
work in the intercept's loader.
|
|
125
|
+
|
|
126
|
+
The intercept handler and its loaders read the context variables global and
|
|
127
|
+
route middleware set.
|
|
124
128
|
|
|
125
129
|
Handler/layout `ctx.set()` data follows the same rule as elsewhere:
|
|
126
130
|
intercepts see data produced in the current render pass, but partial
|
|
@@ -136,7 +140,11 @@ re-render every time the intercept resolves live — each intercepted
|
|
|
136
140
|
navigation, and each action submitted while the modal is open. When the
|
|
137
141
|
segment cache or a prerendered intercept variant serves the modal, the stored
|
|
138
142
|
handler output is reused and only the loaders run. Each loader decides for
|
|
139
|
-
itself through its own `revalidate()`, with the normal loader defaults
|
|
143
|
+
itself through its own `revalidate()`, with the normal loader defaults: an
|
|
144
|
+
action from the modal re-runs only the loaders whose predicate says so, and
|
|
145
|
+
the modal keeps the data of the others, as it does on a link to another
|
|
146
|
+
intercepted route that reads the same loader. In `clientUrls()` the predicate
|
|
147
|
+
runs on the server too (`/client-urls`).
|
|
140
148
|
|
|
141
149
|
A `revalidate()` placed directly in the intercept's `use()` (or returned from
|
|
142
150
|
its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
@@ -144,13 +152,13 @@ its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
|
144
152
|
```typescript
|
|
145
153
|
// Throws: revalidate() is not valid inside intercept("@modal", "product") use()
|
|
146
154
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
147
|
-
revalidate(
|
|
155
|
+
revalidate(false),
|
|
148
156
|
loader(ProductLoader),
|
|
149
157
|
]);
|
|
150
158
|
|
|
151
159
|
// Correct: the loader owns the decision
|
|
152
160
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
153
|
-
loader(ProductLoader, () => [revalidate(
|
|
161
|
+
loader(ProductLoader, () => [revalidate(false)]),
|
|
154
162
|
]);
|
|
155
163
|
```
|
|
156
164
|
|
|
@@ -176,7 +184,7 @@ layout(ProductLayout, () => [
|
|
|
176
184
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
177
185
|
loader(ProductLoader, () => [
|
|
178
186
|
revalidate(revalidateProductShell), // reruns with the producer
|
|
179
|
-
revalidate(
|
|
187
|
+
revalidate(false), // otherwise keep the loaded data
|
|
180
188
|
]),
|
|
181
189
|
]),
|
|
182
190
|
]);
|
|
@@ -197,7 +205,7 @@ export const revalidateProduct = () => [
|
|
|
197
205
|
layout(ProductLayout, () => [
|
|
198
206
|
revalidateProduct(),
|
|
199
207
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
200
|
-
loader(ProductLoader, () => [revalidateProduct(), revalidate(
|
|
208
|
+
loader(ProductLoader, () => [revalidateProduct(), revalidate(false)]),
|
|
201
209
|
]),
|
|
202
210
|
]);
|
|
203
211
|
```
|
|
@@ -244,6 +252,29 @@ back to `Referer`, and only when that `Referer` is on the request's own origin
|
|
|
244
252
|
(scheme, host and port). A request with no usable navigation context is not
|
|
245
253
|
intercepted: it gets the full page.
|
|
246
254
|
|
|
255
|
+
### `prefetch: false` on an intercept's loaders
|
|
256
|
+
|
|
257
|
+
`loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
|
|
258
|
+
of a prefetch of the target from the source page:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
262
|
+
loader(ProductLoader),
|
|
263
|
+
loader(ReviewsLoader, { prefetch: false }), // skipped in the prefetch
|
|
264
|
+
loading(<ModalSkeleton />),
|
|
265
|
+
]);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The click that adopts the prefetch opens the modal at once over the page, with
|
|
269
|
+
its `loading()` while one fill request, sent from the source page, runs the
|
|
270
|
+
flagged loader. The page under the modal is not re-rendered or remounted.
|
|
271
|
+
Without `loading()` the slot shows nothing until the fill answers, as a plain
|
|
272
|
+
click shows nothing until its response. The modal's loaders resolve together,
|
|
273
|
+
so its `loading()` shows until the last one arrives. A handler that reads the
|
|
274
|
+
flagged loader with `ctx.use()` before its first `await` (or one with no
|
|
275
|
+
`loading()`) gets it run in the prefetch instead: one run, no fill for it.
|
|
276
|
+
`loading(…, { prefetch: false })` inside `intercept()` defers nothing.
|
|
277
|
+
|
|
247
278
|
A prefetch of a route an intercept targets is stored per source page, whether
|
|
248
279
|
or not the intercept applies from that page (`rsc-rendering.ts`
|
|
249
280
|
`x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A
|
|
@@ -284,7 +315,7 @@ intercept(
|
|
|
284
315
|
<ProductModal />,
|
|
285
316
|
() => [
|
|
286
317
|
loader(ProductLoader, () => [cache()]),
|
|
287
|
-
loader(ProductCartLoader, () => [revalidate(
|
|
318
|
+
loader(ProductCartLoader, () => [revalidate(true)]),
|
|
288
319
|
loader(RecommendationsLoader),
|
|
289
320
|
]
|
|
290
321
|
)
|
|
@@ -295,15 +326,15 @@ intercept(
|
|
|
295
326
|
An intercept's `use()` (and its handler's `.use`) accepts:
|
|
296
327
|
|
|
297
328
|
- `loader()`, with its own `revalidate()` and `cache()` inside it
|
|
298
|
-
- `middleware()`
|
|
299
329
|
- `loading()`
|
|
300
330
|
- `layout(Chrome)`: the modal chrome, without `use()` items of its own
|
|
301
331
|
- `route`, the item type a `path()` call produces
|
|
302
332
|
- `transition()`
|
|
303
333
|
|
|
304
334
|
Everything else throws at definition time with a pointer to where it goes:
|
|
305
|
-
`
|
|
306
|
-
|
|
335
|
+
`middleware()` onto the target route, `revalidate()` onto the loader,
|
|
336
|
+
`errorBoundary()` / `notFoundBoundary()` onto the enclosing layout (see
|
|
337
|
+
below), and `cache()` onto the target route.
|
|
307
338
|
|
|
308
339
|
## Caching an Intercept
|
|
309
340
|
|
|
@@ -526,7 +557,7 @@ export const shopPatterns = urls(({
|
|
|
526
557
|
|
|
527
558
|
## Handler-attached `.use`
|
|
528
559
|
|
|
529
|
-
Intercept handlers can carry their own
|
|
560
|
+
Intercept handlers can carry their own 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).)
|
|
530
561
|
|
|
531
562
|
```typescript
|
|
532
563
|
import { layout, loader, loading, type Handler } from "@rangojs/router";
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -234,7 +234,9 @@ among the path's children (`/caching`).
|
|
|
234
234
|
A standalone `layout()` renders when it is first mounted and is then kept as-is:
|
|
235
235
|
|
|
236
236
|
- **Navigation** between routes it already wraps does not re-render it — even
|
|
237
|
-
when the URL params change.
|
|
237
|
+
when the URL params change. Links it rendered with `ctx.reverse` (mount
|
|
238
|
+
params auto-filled) keep the old params too; render param-dependent nav in a
|
|
239
|
+
client component with `useReverse(routes)` (`/links`).
|
|
238
240
|
- **Actions** skip it too (the revalidation trace calls this
|
|
239
241
|
`action:parent-chain-skip`).
|
|
240
242
|
|
|
@@ -264,9 +266,10 @@ layout(LocaleLayout, () => [
|
|
|
264
266
|
])
|
|
265
267
|
```
|
|
266
268
|
|
|
267
|
-
`revalidate(
|
|
269
|
+
`revalidate(false)` on a standalone layout only restates the default.
|
|
268
270
|
The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
|
|
269
|
-
are covered in `/loader` →
|
|
271
|
+
and the `revalidate(true | false)` shorthand are covered in `/loader` →
|
|
272
|
+
"`revalidate()` return shapes".
|
|
270
273
|
|
|
271
274
|
If child segments read data that was established by this layout or by a
|
|
272
275
|
route handler above them, revalidate the outer segment too. Partial
|