@rangojs/router 0.32.0 → 0.33.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 +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -18
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
package/skills/links/SKILL.md
CHANGED
|
@@ -457,7 +457,7 @@ export const useAccountReverse = () => useReverse(accountRoutes);
|
|
|
457
457
|
Components call `useAccountReverse()`; a renamed or moved module changes one
|
|
458
458
|
file. The wrapper resolves like `useReverse`: against the nearest `include()`
|
|
459
459
|
mount, so call a module's hook from components under that module's own mount.
|
|
460
|
-
Inside a nested module it builds a wrong URL with no error
|
|
460
|
+
Inside a nested module it builds a wrong URL with no error; pass
|
|
461
461
|
cross-module links from the server there.
|
|
462
462
|
|
|
463
463
|
### How it resolves
|
|
@@ -542,7 +542,7 @@ Both happen synchronously during `reverse()` — wrap calls in try/catch (or an
|
|
|
542
542
|
|
|
543
543
|
### The leading dot is optional
|
|
544
544
|
|
|
545
|
-
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. A module's `routes` map is right only inside that module's own mount: it resolves against the current mount, so another module's map gives a wrong URL with no error, and `useReverse(NamedRoutes)` is correct only at the root mount (under a mount it double-prefixes). There is no client API for links that cross modules under a nested mount
|
|
545
|
+
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. A module's `routes` map is right only inside that module's own mount: it resolves against the current mount, so another module's map gives a wrong URL with no error, and `useReverse(NamedRoutes)` is correct only at the root mount (under a mount it double-prefixes). There is no client API for links that cross modules under a nested mount. Build them on the server, where global names resolve fully, and pass them down:
|
|
546
546
|
|
|
547
547
|
```tsx
|
|
548
548
|
// server layout or handler
|
|
@@ -619,7 +619,8 @@ A prefetch is a navigation sent early (a partial request with an
|
|
|
619
619
|
`X-Rango-Prefetch` header), and by default the server renders everything for
|
|
620
620
|
it: every handler and every loader. That is the point, a click then commits
|
|
621
621
|
without a request, but it is also forty renders for a grid of forty links.
|
|
622
|
-
Keep the expensive part out of
|
|
622
|
+
Keep the expensive part out of speculative (viewport, render) prefetches on
|
|
623
|
+
the route, not on the link:
|
|
623
624
|
|
|
624
625
|
```tsx
|
|
625
626
|
// skip one loader
|
|
@@ -632,11 +633,107 @@ A click that adopts such a prefetch commits what was prefetched, shows the
|
|
|
632
633
|
fallback for the deferred part, and sends one extra request for it. Only what
|
|
633
634
|
is new to the page is deferred: a segment already on screen (the layout of the
|
|
634
635
|
section you are in, the route itself on `/product/a` to `/product/b`) is
|
|
635
|
-
prefetched as usual.
|
|
636
|
+
prefetched as usual.
|
|
637
|
+
|
|
638
|
+
A hover prefetch is treated as intent and runs everything, flagged work
|
|
639
|
+
included (`X-Rango-Prefetch: intent`, where a viewport or render prefetch
|
|
640
|
+
sends `1`); so does `useRouter().prefetch()`. Its click commits complete with
|
|
641
|
+
no extra request. A hover sends that request only when the link has no
|
|
642
|
+
prefetch cached or in flight: on a viewport link whose prefetch has already
|
|
643
|
+
answered, it sends nothing, and the click fetches the deferred part as above.
|
|
644
|
+
See
|
|
636
645
|
`/loader` → "`prefetch: false`" and `/route` → "`loading()` options". To stop
|
|
637
646
|
a link or a whole section from prefetching at all, use the `prefetch` prop or
|
|
638
647
|
`data-prefetch-scope` above.
|
|
639
648
|
|
|
649
|
+
### Shared prefetches and `prefetchKey`
|
|
650
|
+
|
|
651
|
+
A prefetch is stored for its target URL and shared by every page. A link to
|
|
652
|
+
`/products/42` prefetched on the home page is not fetched again on the search
|
|
653
|
+
page or on page 2 of a list, and a click from any of them adopts that one
|
|
654
|
+
entry. This is deliberate: without it, a paginated list or a product grid
|
|
655
|
+
would prefetch every visible link again after each navigation.
|
|
656
|
+
|
|
657
|
+
The answer is a diff against the page that asked. The server leaves out the
|
|
658
|
+
segments that page already shows, and the click fills them from the copies on
|
|
659
|
+
screen. A shared answer is right on every page as long as that diff does not
|
|
660
|
+
depend on which page asked. With the defaults it does not: a standalone layout
|
|
661
|
+
is held from every page that shows it, and a route, with any layout inside its
|
|
662
|
+
`path()`, re-renders whenever its params or search change.
|
|
663
|
+
|
|
664
|
+
When the diff does depend on the page, the server scopes the answer. A
|
|
665
|
+
click from a page outside the scope then sends its own request.
|
|
666
|
+
|
|
667
|
+
The answer is page-only (`x-rsc-prefetch-scope: source`) when:
|
|
668
|
+
|
|
669
|
+
- An intercept targets the route, or the page that sent the prefetch,
|
|
670
|
+
whether or not it applies here.
|
|
671
|
+
- The answer defers a `prefetch: false` loader or `loading()` that the page
|
|
672
|
+
does not show yet (a viewport or render prefetch; a hover's defers nothing).
|
|
673
|
+
- A `revalidate()` on the target read `currentUrl` and kept its segment.
|
|
674
|
+
- A `revalidate()` on the route, or on a segment inside its `path()`, read
|
|
675
|
+
the page being left and kept the segment through the route's default
|
|
676
|
+
(it returned `undefined`, or the `defaultShouldRevalidate` it was given).
|
|
677
|
+
That default compares the target's params and search with the page's,
|
|
678
|
+
which no read records.
|
|
679
|
+
- The answer kept a segment for the page that asked and sends no segment at
|
|
680
|
+
all: a page that adopted it would keep its whole tree.
|
|
681
|
+
|
|
682
|
+
The answer is shared between a group of pages
|
|
683
|
+
(`x-rsc-prefetch-scope: params; <label>`) when:
|
|
684
|
+
|
|
685
|
+
- A `revalidate()` on the target read `currentParams` or `fromRouteName`
|
|
686
|
+
and kept its segment. That keep was decided from what the rule read, so it
|
|
687
|
+
is right on every page with equal values of those reads, and only there:
|
|
688
|
+
another page holds a copy rendered for other params. The server records
|
|
689
|
+
each param a rule reads by name (a spread, `JSON.stringify` or
|
|
690
|
+
`structuredClone` reads them all), the route name for `fromRouteName`, and
|
|
691
|
+
the page's set of params, and answers with the values; the browser stores
|
|
692
|
+
the answer under them and looks it up with the params of the page on
|
|
693
|
+
screen. The locale rule (`currentParams.locale !== nextParams.locale`)
|
|
694
|
+
shares each prefetch between the pages of one locale.
|
|
695
|
+
- The target is prerendered and the page that asked has the same route
|
|
696
|
+
params: the stored answer keeps a layout that has `revalidate()` rules
|
|
697
|
+
without running them, which is right for the pages with the same params.
|
|
698
|
+
|
|
699
|
+
A scoped answer, page-only or grouped, is sent `cache-control: private,
|
|
700
|
+
no-cache` over the route's own header, and the document cache does not store
|
|
701
|
+
it: a shared cache keys a navigation by its URL, not by the page that sent it
|
|
702
|
+
(`/document-cache` → "What's NOT Cached").
|
|
703
|
+
|
|
704
|
+
Page-only entries are keyed by the page's full URL, query string and hash
|
|
705
|
+
included, so each page prefetches those targets again: `?page=2` and `?page=3`
|
|
706
|
+
each make their own. Grouped entries are shared by `?page=2` and `?page=3`
|
|
707
|
+
when the rule did not read the page. The answer stays correct either way;
|
|
708
|
+
the cost is the sharing. Read the params a rule needs by name and avoid
|
|
709
|
+
`currentUrl` in a rule on a layout above most of the app.
|
|
710
|
+
`/app-structure` → "Prefetches are shared" lists the structures that need no
|
|
711
|
+
such rule.
|
|
712
|
+
|
|
713
|
+
`prefetchKey=":source"` asks for the same scope on one link, for a target
|
|
714
|
+
whose answer depends on the page that sent it in a way the server cannot
|
|
715
|
+
see:
|
|
716
|
+
|
|
717
|
+
```tsx
|
|
718
|
+
<Link to={href} prefetchKey=":source">
|
|
719
|
+
Back to results
|
|
720
|
+
</Link>;
|
|
721
|
+
// The same for a programmatic prefetch
|
|
722
|
+
router.prefetch(href, { key: ":source" });
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
The server tracks only the three fields above. Use the key when the
|
|
726
|
+
dependency runs through anything else:
|
|
727
|
+
|
|
728
|
+
- `context` in a `revalidate()` (the handler context: the request, its
|
|
729
|
+
headers and cookies, and middleware variables);
|
|
730
|
+
- a handler, loader or middleware that reads the request's `Referer`, which
|
|
731
|
+
the browser sets to the page that sent the prefetch (unless the page's
|
|
732
|
+
`Referrer-Policy` strips it).
|
|
733
|
+
|
|
734
|
+
`":source"` is the only value. It costs the same as a page-only entry, so put
|
|
735
|
+
it on the links that need it, never on every link of an app.
|
|
736
|
+
|
|
640
737
|
## When to use what
|
|
641
738
|
|
|
642
739
|
| Context | API | Resolves | Use for |
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -218,7 +218,11 @@ Loaders receive a request-scoped context that shares its read surface with
|
|
|
218
218
|
route handlers (`params`, `request`, `url`, `env`, `get`, `use`, `reverse`).
|
|
219
219
|
It has no handler-only writers: there is no `ctx.set`, `ctx.headers`, or
|
|
220
220
|
`ctx.setLocationState` (a loader can stream after the response has started,
|
|
221
|
-
so response-shaping belongs in middleware or the handler).
|
|
221
|
+
so response-shaping belongs in middleware or the handler). Calling
|
|
222
|
+
`getRequestContext().setLocationState()` from a loader is not supported
|
|
223
|
+
either: the payload takes the request's location state before loaders
|
|
224
|
+
settle, so such a write may arrive or may be dropped, and a `ppr` shell HIT
|
|
225
|
+
never sends it (see `/hooks` `state.md`, "State on a document response").
|
|
222
226
|
|
|
223
227
|
### Full field surface
|
|
224
228
|
|
|
@@ -490,7 +494,11 @@ the same `$id ?? $$id` precedence the router uses to derive `actionId`, so it
|
|
|
490
494
|
matches in both dev and production. The raw `actionId` string stays available on
|
|
491
495
|
the same context as an escape hatch, alongside `actionResult`, `formData`,
|
|
492
496
|
`actionUrl`, `currentParams`/`nextParams`, `currentUrl`/`nextUrl`,
|
|
493
|
-
`fromRouteName`/`toRouteName`, and the full handler `context`.
|
|
497
|
+
`fromRouteName`/`toRouteName`, and the full handler `context`. Reading
|
|
498
|
+
`currentParams` or `fromRouteName` and keeping the segment shares a prefetch
|
|
499
|
+
of that answer only between the pages with equal values of what was read;
|
|
500
|
+
reading `currentUrl` makes it page-only (`/links` → "Shared prefetches and
|
|
501
|
+
`prefetchKey`"); `context` is not tracked.
|
|
494
502
|
|
|
495
503
|
### Revalidation Contracts for Loader Dependencies
|
|
496
504
|
|
|
@@ -1183,20 +1191,20 @@ The costs and constraints:
|
|
|
1183
1191
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
1184
1192
|
loader-heavy shape makes it most useful.
|
|
1185
1193
|
|
|
1186
|
-
## `prefetch: false` — Keep a Loader Out of Prefetches
|
|
1194
|
+
## `prefetch: false` — Keep a Loader Out of Speculative Prefetches
|
|
1187
1195
|
|
|
1188
1196
|
A `<Link>` prefetch is a navigation sent early: the server renders every
|
|
1189
1197
|
handler and every loader for it. With the default `"viewport"` strategy that
|
|
1190
1198
|
means a grid of forty product links runs forty copies of your slowest loader
|
|
1191
1199
|
for pages most visitors never open. Pass `prefetch: false` to keep one loader
|
|
1192
|
-
out:
|
|
1200
|
+
out of those speculative (viewport and render) prefetches:
|
|
1193
1201
|
|
|
1194
1202
|
```tsx
|
|
1195
1203
|
import { Suspense } from "react";
|
|
1196
1204
|
|
|
1197
1205
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1198
1206
|
loader(PriceLoader), // cheap: still runs in the prefetch
|
|
1199
|
-
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in a prefetch
|
|
1207
|
+
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in a viewport prefetch
|
|
1200
1208
|
loading(<ProductSkeleton />),
|
|
1201
1209
|
]),
|
|
1202
1210
|
```
|
|
@@ -1204,11 +1212,22 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
1204
1212
|
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1205
1213
|
`{ ssr: false, prefetch: false }` if you need both.)
|
|
1206
1214
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1215
|
+
A hover is different: the visitor is about to click. A hover prefetch (on a
|
|
1216
|
+
`"hover"`, `"viewport"` or `"adaptive"` link, or a delegated plain anchor) and
|
|
1217
|
+
`useRouter().prefetch()` send `X-Rango-Prefetch: intent` and run the loader
|
|
1218
|
+
with the rest of the page, so the click commits complete and sends no second
|
|
1219
|
+
request. A hover sends its prefetch only when the link has none cached or in
|
|
1220
|
+
flight: on a link a viewport prefetch already answered, the hover sends
|
|
1221
|
+
nothing and the click fills the deferred loader as below. The cost is plain:
|
|
1222
|
+
each hover that misses the prefetch cache runs the loader, so a pointer
|
|
1223
|
+
passing over a list of `"hover"` links runs it once per link. Touch devices
|
|
1224
|
+
have no hover (`"adaptive"` resolves to viewport there) and keep the fill.
|
|
1225
|
+
|
|
1226
|
+
What happens in a speculative prefetch:
|
|
1227
|
+
|
|
1228
|
+
- A **viewport or render prefetch** of the route does not run the loader and
|
|
1229
|
+
does not read its `cache()`; the payload marks it deferred. The rest of the
|
|
1230
|
+
page prefetches as usual.
|
|
1212
1231
|
- Only on a loader new to the page. A loader already on screen is never
|
|
1213
1232
|
deferred: if its revalidation (its `revalidate()` or the default) re-runs
|
|
1214
1233
|
it, it runs in the prefetch with the rest of the destination, exactly as
|
|
@@ -1258,16 +1277,18 @@ function Reviews() {
|
|
|
1258
1277
|
|
|
1259
1278
|
The limits:
|
|
1260
1279
|
|
|
1261
|
-
- **A click before its prefetch has answered waits for it, then
|
|
1262
|
-
deferred part.** The flag is there to keep expensive or slow
|
|
1263
|
-
every prefetch, so that an API that is costly to
|
|
1264
|
-
every link a visitor might click. A click that comes
|
|
1265
|
-
prefetch is still in flight reuses that
|
|
1280
|
+
- **A click before its viewport prefetch has answered waits for it, then
|
|
1281
|
+
fetches the deferred part.** The flag is there to keep expensive or slow
|
|
1282
|
+
work out of every speculative prefetch, so that an API that is costly to
|
|
1283
|
+
call is not called for every link a visitor might click. A click that comes
|
|
1284
|
+
while the link's viewport or render prefetch is still in flight reuses that
|
|
1285
|
+
prefetch, as it does without the
|
|
1266
1286
|
flag, and sends the fill when the prefetch answers. The page shows no later
|
|
1267
1287
|
than with no prefetch; the deferred part shows its fallback until the fill
|
|
1268
1288
|
lands, and can complete later than a click with no prefetch, by up to what
|
|
1269
1289
|
was left of the prefetch. That is the price of keeping the work out of the
|
|
1270
|
-
prefetch.
|
|
1290
|
+
prefetch. A click while a hover's prefetch is in flight adopts it with no
|
|
1291
|
+
fill: the loader is already running in it.
|
|
1271
1292
|
- **A navigation started before the fill answers cancels it.** It is
|
|
1272
1293
|
cancelled when the newer navigation starts, not when that one commits. If
|
|
1273
1294
|
the newer navigation then ends without replacing the page (a redirect the
|
|
@@ -1360,8 +1381,9 @@ The limits:
|
|
|
1360
1381
|
data with the URL, and one fill runs the flagged loaders (see
|
|
1361
1382
|
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1362
1383
|
on a group route, from the group or from a server layout above it.
|
|
1363
|
-
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a
|
|
1364
|
-
the intercept's target from its source page defers the
|
|
1384
|
+
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a viewport or
|
|
1385
|
+
render prefetch of the intercept's target from its source page defers the
|
|
1386
|
+
flagged loader (a hover's runs it, and its click sends no fill): the
|
|
1365
1387
|
click opens the modal at once, with its `loading()`, and one fill, sent
|
|
1366
1388
|
from the source, runs the loader. The page under the modal stays. With no
|
|
1367
1389
|
`loading()` the slot is empty until the fill answers, as on a plain click
|
|
@@ -1371,16 +1393,21 @@ The limits:
|
|
|
1371
1393
|
`revalidate()`, so an action from the modal re-runs only those that say so
|
|
1372
1394
|
(see `/intercept`).
|
|
1373
1395
|
- **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
|
|
1374
|
-
bake lane
|
|
1375
|
-
it
|
|
1376
|
-
|
|
1396
|
+
bake lane. A prefetch that replays a shell a document request captured
|
|
1397
|
+
serves it from its pin: it is not deferred, does not run, and the click
|
|
1398
|
+
sends nothing. Every other prefetch (a navigation-only shell, a cold page,
|
|
1399
|
+
an explicit `cache()` hit) defers it as above, and the click sends one
|
|
1400
|
+
fill. Under `cache()` and `Prerender` the flag works as above (see
|
|
1401
|
+
`/caching`, `/prerender`, `/ppr` → "`prefetch: false` on a `ppr` route").
|
|
1377
1402
|
|
|
1378
1403
|
To skip a handler along with its loaders, flag the `loading()` instead (see
|
|
1379
1404
|
`/route` → "`loading()` options"). `loader(Def, { prefetch: false })` accepts
|
|
1380
1405
|
only a boolean; anything else throws `loader() prefetch must be a boolean`.
|
|
1381
1406
|
|
|
1382
1407
|
Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
|
|
1383
|
-
and `result.readDeferred()` (see `/testing`).
|
|
1408
|
+
and `result.readDeferred()` (see `/testing`). That is a speculative prefetch;
|
|
1409
|
+
add `headers: { "X-Rango-Prefetch": "intent" }` to serve a hover's, which
|
|
1410
|
+
defers nothing.
|
|
1384
1411
|
|
|
1385
1412
|
## Fetchable Loaders
|
|
1386
1413
|
|
|
@@ -649,6 +649,11 @@ parallel({ "@sidebar": BlogSidebar }, () => [
|
|
|
649
649
|
]);
|
|
650
650
|
```
|
|
651
651
|
|
|
652
|
+
The sidebar rule reads `currentParams` and keeps the sidebar on an unchanged
|
|
653
|
+
`:slug`, so a prefetch of the same post (another tab, `?sort=`) is shared by
|
|
654
|
+
the pages of that post: the rule read its slug. Links to other posts stay
|
|
655
|
+
shared. `/links` → "Shared prefetches and `prefetchKey`".
|
|
656
|
+
|
|
652
657
|
**Server-side caching** — `cache()` DSL, loader-level `cache()`, and `"use cache"`
|
|
653
658
|
control what gets cached and for how long. This is separate from `revalidate()`:
|
|
654
659
|
|
|
@@ -833,8 +838,9 @@ router cache — map to Rango's prefetch system: per-Link
|
|
|
833
838
|
option) warms the target's partial RSC payload before the click, and a click
|
|
834
839
|
on a warmed link commits the prefetched payload as a whole — the complete
|
|
835
840
|
page lands instantly, no fetch waterfall. Prefetched entries survive being
|
|
836
|
-
used (they re-arm in place) and expire by `prefetchCacheTTL
|
|
837
|
-
`invalidateClientCache()`
|
|
841
|
+
used (they re-arm in place) and expire by `prefetchCacheTTL`.
|
|
842
|
+
`invalidateClientCache()` flushes them; a server action marks them stale, so
|
|
843
|
+
a click on one shows it and then revalidates the page in the background.
|
|
838
844
|
|
|
839
845
|
```tsx
|
|
840
846
|
<Link to="/product/widget" prefetch="viewport">
|
|
@@ -245,6 +245,11 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
245
245
|
]);
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
+
On an unchanged `:slug` this rule keeps the route after reading `currentParams`,
|
|
249
|
+
so a prefetch of the same product (a tab, `?sort=price`) is shared by the
|
|
250
|
+
pages of that product: the rule read its slug. Links to other products stay
|
|
251
|
+
shared. `/links` → "Shared prefetches and `prefetchKey`".
|
|
252
|
+
|
|
248
253
|
Note: RR7's `shouldRevalidate` controls client-side loader re-fetching. Rango's
|
|
249
254
|
`revalidate()` controls which segments re-run during partial rendering after
|
|
250
255
|
navigation or actions. The intent is the same — skip unnecessary work — but
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -512,6 +512,13 @@ full: a HIT runs no handler, so a handler's `ctx.setLocationState()` does not
|
|
|
512
512
|
happen on a HIT. Set it from middleware (see `/hooks` `state.md`, "State on a
|
|
513
513
|
document response").
|
|
514
514
|
|
|
515
|
+
A HIT sends exactly the state set before its render: its headers, which
|
|
516
|
+
make it `private, no-store`, leave before the tail runs, so a write during
|
|
517
|
+
the tail (a loader calling `getRequestContext().setLocationState()`, which
|
|
518
|
+
is not supported) is not sent. A HIT that read a document redirect's cookie
|
|
519
|
+
and then degrades into a reload sets the cookie again from its reload
|
|
520
|
+
script, so the forced MISS still carries the state.
|
|
521
|
+
|
|
515
522
|
### Opting out per request with `ctx.dynamic()`
|
|
516
523
|
|
|
517
524
|
Middleware and handlers can call `ctx.dynamic()` to force this request onto a
|
|
@@ -1158,11 +1165,25 @@ A `ppr` route's handler output is stored, so a `loading()` flagged with
|
|
|
1158
1165
|
and its partial replay are served as usual and only the live loaders behind
|
|
1159
1166
|
the fallback are deferred. A live loader registered with `{ prefetch: false }` is
|
|
1160
1167
|
skipped the same way, and the click that adopts the prefetch sends one fill
|
|
1161
|
-
request for it.
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1168
|
+
request for it.
|
|
1169
|
+
|
|
1170
|
+
An `ssr: false` loader (the bake lane) registered with `{ prefetch: false }`
|
|
1171
|
+
is deferred too, unless the prefetch serves it from a pin of a shell a
|
|
1172
|
+
document request captured:
|
|
1173
|
+
|
|
1174
|
+
| The prefetch | The flagged `ssr: false` loader |
|
|
1175
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------- |
|
|
1176
|
+
| Replays a shell a document request captured (`HIT`) | Served from its pin, not deferred, never run; the click sends nothing |
|
|
1177
|
+
| Replays a navigation-only shell, or misses (cold page) | Deferred: the click sends one fill that runs it |
|
|
1178
|
+
| Serves an explicit `cache()` hit | Deferred: the click sends one fill that runs it |
|
|
1179
|
+
| Hover prefetch (`X-Rango-Prefetch: intent`) | Never deferred |
|
|
1180
|
+
|
|
1181
|
+
A cold prefetch still schedules the background capture that stores the
|
|
1182
|
+
page's navigation shell, and that capture runs the loader once. Once a
|
|
1183
|
+
document request has captured the page's shell, prefetches replay it and
|
|
1184
|
+
serve the loader from its pin, as without the flag. Since such a loader can be deferred, it must not
|
|
1185
|
+
`await ctx.rendered()` (it throws on every request, as any deferrable loader
|
|
1186
|
+
does). See `/loader` → "`prefetch: false`".
|
|
1166
1187
|
|
|
1167
1188
|
### The layout-with-loaders playbook (the storefront case)
|
|
1168
1189
|
|
|
@@ -1020,7 +1020,10 @@ in dev. A removed page is the exception: its marker answers 404 in dev too.
|
|
|
1020
1020
|
that only a refresh produced.
|
|
1021
1021
|
- **Connected clients can lag.** A refresh has no response to reach open tabs:
|
|
1022
1022
|
a client that prefetched the page keeps its copy for `prefetchCacheTTL`
|
|
1023
|
-
(default 300s
|
|
1023
|
+
(default 300s): in memory, and in the browser's HTTP cache too when the
|
|
1024
|
+
route runs no loader (the prefetch then carries
|
|
1025
|
+
`Cache-Control: private, max-age=<prefetchCacheTTL>`). Lower it if the
|
|
1026
|
+
window matters.
|
|
1024
1027
|
- **Entries are per deploy.** Keys carry the router's data version, so a deploy
|
|
1025
1028
|
that changes server code starts from the build entries again; a client-only
|
|
1026
1029
|
deploy keeps refreshed pages.
|
package/skills/route/SKILL.md
CHANGED
|
@@ -242,10 +242,10 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
242
242
|
|
|
243
243
|
`loading(fallback, options?)` takes two options:
|
|
244
244
|
|
|
245
|
-
| Option | Effect
|
|
246
|
-
| ----------------- |
|
|
247
|
-
| `ssr: false` | Skip the fallback on document requests; it shows on client navigations only.
|
|
248
|
-
| `prefetch: false` | Keep everything behind this fallback out of
|
|
245
|
+
| Option | Effect |
|
|
246
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `ssr: false` | Skip the fallback on document requests; it shows on client navigations only. |
|
|
248
|
+
| `prefetch: false` | Keep everything behind this fallback out of viewport and render prefetches (a hover prefetch runs it); the click shows the fallback and fetches that part itself. |
|
|
249
249
|
|
|
250
250
|
```tsx
|
|
251
251
|
path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
@@ -255,8 +255,8 @@ path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
**What `prefetch: false` defers.** The flag applies when the entry is new to
|
|
258
|
-
the browser: the page the prefetch is sent from does not show it. A
|
|
259
|
-
then skips the entry's handler, its loaders, its orphan layouts and its
|
|
258
|
+
the browser: the page the prefetch is sent from does not show it. A viewport
|
|
259
|
+
or render prefetch then skips the entry's handler, its loaders, its orphan layouts and its
|
|
260
260
|
parallel slots. On a `layout()` the fallback covers the whole outlet, so every
|
|
261
261
|
deeper entry is skipped with it. A parallel slot with its own flagged
|
|
262
262
|
`loading()` is a separate unit: the route beside it still renders in the
|
|
@@ -250,7 +250,8 @@ Notes:
|
|
|
250
250
|
- `discovery: { include?, exclude? }` filters which files the route-discovery
|
|
251
251
|
scan reads, as root-relative globs (e.g. `include: ["src/routes/**"]`).
|
|
252
252
|
`exclude` replaces the default excludes (`__tests__`, `__mocks__`, `dist`,
|
|
253
|
-
`coverage`, `*.test.*`, `*.spec.*`) rather than adding to
|
|
253
|
+
`coverage`, `*.test.*`, `*.rsc-test.*`, `*.spec.*`) rather than adding to
|
|
254
|
+
them.
|
|
254
255
|
- `progressiveChunkSize` sets React's Fizz `progressiveChunkSize` for document
|
|
255
256
|
renders and PPR shell capture. Unset, documents whose route has a
|
|
256
257
|
`loader(Def, { ssr: false })` raise it automatically so that content stays
|
|
@@ -531,11 +531,42 @@ See `/middleware` for the full cross-segment revalidation contract.
|
|
|
531
531
|
## Client Cache After an Action
|
|
532
532
|
|
|
533
533
|
`revalidate()` decides what the server re-renders **in the action's response**.
|
|
534
|
-
Separately, the browser
|
|
535
|
-
arrives:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
534
|
+
Separately, the browser marks its own caches stale once the action's response
|
|
535
|
+
arrives: the history cache (Back renders the cached entry, then revalidates)
|
|
536
|
+
and every prefetch (a click on a link prefetched before the action shows that
|
|
537
|
+
page at once, then revalidates it in the background), and it tells other open
|
|
538
|
+
tabs that share a segment with the page to do the same and refresh. Nothing is
|
|
539
|
+
flushed. The action's response bumps only the write part of the router's state
|
|
540
|
+
cookie (`{build}:{invalidation}:{write}`), which every prefetch and navigation
|
|
541
|
+
sends as `X-Rango-State`: responses vary on it, so a partial response a route
|
|
542
|
+
caches with its own `Cache-Control: max-age` is not answered from the
|
|
543
|
+
browser's HTTP cache after the write, in this tab, after a reload, or in
|
|
544
|
+
another tab. The client's in-memory caches read only
|
|
545
|
+
`{build}:{invalidation}`, so the write marks them stale and drops nothing.
|
|
546
|
+
This happens for every action that reached the server, including ones that
|
|
547
|
+
threw or redirected, and for a form posted before hydration. No setup is
|
|
548
|
+
needed.
|
|
549
|
+
|
|
550
|
+
```text
|
|
551
|
+
/product/1: /cart prefetched (cart:2), click "Add to cart"
|
|
552
|
+
the action's response (header: cart:3)
|
|
553
|
+
click the /cart link /cart shows at once (cart:2)
|
|
554
|
+
one background revalidation (cart:3)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The pre-write values show for one round trip, a layout loader that re-runs
|
|
558
|
+
on navigation included. `useNavigation().state` stays `"idle"` during the
|
|
559
|
+
revalidation, and nothing remounts. A new hover on the link replaces the
|
|
560
|
+
stale prefetch with a fresh one. A stale prefetch answers one click only: a
|
|
561
|
+
second click on the same link fetches the page, and Back/Forward onto a page
|
|
562
|
+
the history cache no longer holds never uses it. Visible links do not
|
|
563
|
+
prefetch again after an action. A tab that shares no segment with the page
|
|
564
|
+
(or an app with `crossTabSync: false`) gets no message and does not refresh;
|
|
565
|
+
its next prefetch or navigation sees the bumped state and marks its own
|
|
566
|
+
history and prefetches stale, so a click on a link it prefetched before the
|
|
567
|
+
write shows that page, then revalidates it. Call `invalidateClientCache()` in the action
|
|
568
|
+
for the full flush: it drops every prefetch, rotates the state's invalidation
|
|
569
|
+
part, and other tabs adopt it.
|
|
539
570
|
|
|
540
571
|
### When the user navigates while the action runs
|
|
541
572
|
|
|
@@ -596,9 +627,11 @@ export async function saveDraft(formData: FormData) {
|
|
|
596
627
|
```
|
|
597
628
|
|
|
598
629
|
What it suppresses, for this action only: the stale-marking of the history
|
|
599
|
-
cache
|
|
600
|
-
and the background
|
|
601
|
-
the
|
|
630
|
+
cache and of the prefetches, the state cookie's write bump, the cross-tab
|
|
631
|
+
broadcast, and the background
|
|
632
|
+
revalidation the client runs when the user navigated while the action was in
|
|
633
|
+
flight (above). A click on a prefetch made before it shows that prefetch with
|
|
634
|
+
no revalidation. What it does not change:
|
|
602
635
|
|
|
603
636
|
- The action's own response still carries the revalidation render your
|
|
604
637
|
`revalidate()` rules select, and the client applies it.
|
|
@@ -79,6 +79,21 @@ type RenderRouteResult = RenderResult & {
|
|
|
79
79
|
// re-render the current location, as router.refresh() does:
|
|
80
80
|
// a spec's when gets kind "revalidate" (`to` is `from`)
|
|
81
81
|
refresh(): Promise<void>;
|
|
82
|
+
// keep `url` as a prefetch made now carries it (the seeds then, `loaders`
|
|
83
|
+
// over them); a later navigate(url) without `loaders` commits that data
|
|
84
|
+
prefetch(
|
|
85
|
+
url: string,
|
|
86
|
+
options?: {
|
|
87
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
88
|
+
},
|
|
89
|
+
): void;
|
|
90
|
+
// land a server action's response: `loaders` are the post-write values,
|
|
91
|
+
// the page on screen re-renders with them, and (without keepClientCache)
|
|
92
|
+
// every history entry and prefetch is marked stale
|
|
93
|
+
action(options?: {
|
|
94
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
95
|
+
keepClientCache?: boolean;
|
|
96
|
+
}): Promise<void>;
|
|
82
97
|
pathname(): string;
|
|
83
98
|
params(): Record<string, string>;
|
|
84
99
|
store: NavigationStore; // advanced
|
|
@@ -420,19 +435,20 @@ it("dims the held price while the next product streams", async () => {
|
|
|
420
435
|
});
|
|
421
436
|
```
|
|
422
437
|
|
|
423
|
-
| Pending state
|
|
424
|
-
|
|
|
425
|
-
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed
|
|
426
|
-
| `transition` navigate(), settled seed (a plain value)
|
|
427
|
-
| navigate() without `transition`, pending seed
|
|
428
|
-
| `transition: { when }` returning false, or `navigate(url, { transition: false })`
|
|
429
|
-
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic)
|
|
430
|
-
| `useNavigation().state`, `useLinkStatus().pending`
|
|
431
|
-
| `useAction().state`
|
|
432
|
-
| `useHandle()` on a held navigation, `navigate(url, { handles, loaders })`
|
|
433
|
-
| `useNavigation().isStreaming` past the commit, `navigate(url, { streamUntil })`
|
|
434
|
-
| A navigation started while React holds a page (a newer click, a `transition: false` commit)
|
|
435
|
-
| A back/forward restored from the history cache (`pop`)
|
|
438
|
+
| Pending state | Under `renderRoute` |
|
|
439
|
+
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
440
|
+
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
|
|
441
|
+
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
|
|
442
|
+
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
|
|
443
|
+
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
|
|
444
|
+
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
|
|
445
|
+
| `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until that commit, held with it too. |
|
|
446
|
+
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
|
|
447
|
+
| `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
|
|
448
|
+
| `useNavigation().isStreaming` past the commit, `navigate(url, { streamUntil })` | Modeled: the stream opens a task after the call (`pendingUrl` clears) and ends when the promise settles or a newer navigation aborts it; settle it inside `await act(async () => ...)`. |
|
|
449
|
+
| A navigation started while React holds a page (a newer click, a `transition: false` commit) | `await router.navigate(...)` shows its page, with or without `act()`. A `<Link>` click goes inside `await act(async () => { ... })`: `fireEvent`'s synchronous `act()` drops React's retry of the held page. |
|
|
450
|
+
| A back/forward restored from the history cache (`pop`) | Modeled for the mounted entry and one a navigate() wrote: the page and its handle values come back, decided by `transition({ when })` as kind `"pop"`. A stale entry's revalidation and a restore that fetches: e2e. |
|
|
451
|
+
| A click on a prefetch made before an action, `prefetch(url)`, `action({ loaders })`, `navigate(url)` | Modeled: the click commits the prefetch's data at once; once it settled, the page revalidates in Back's lane with the post-write data (`useNavigation().state` stays `idle`, nothing remounts). `router.revalidations()` lists each revalidation request (its URL and held segment ids), decided by production's guards. `action({ keepClientCache: true })` keeps the prefetch fresh: no revalidation. The prefetch request itself: e2e. |
|
|
436
452
|
|
|
437
453
|
- Per-navigation seeds merge over the render-time `loaders`/`loaderData` for THAT navigation only; a later `navigate()` without `loaders` falls back to the render-time data.
|
|
438
454
|
- Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
|