@rangojs/router 0.33.1 → 0.34.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.
Files changed (63) hide show
  1. package/dist/bin/{chunk-TTXC2U3G.js → chunk-4HHFQSDC.js} +10 -2
  2. package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
  3. package/dist/bin/{client-urls-projection-DQLWPDAT.js → client-urls-projection-4Q5EQDAL.js} +2 -2
  4. package/dist/bin/rango.js +1 -1
  5. package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
  6. package/dist/bin/{runtime-discovery-3HH6BP3K.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
  7. package/dist/bin/{temp-rsc-server-JALTIWLT.js → temp-rsc-server-EUNKRFJD.js} +3 -3
  8. package/dist/testing/vitest.js +5 -0
  9. package/dist/types/browser/history-state.d.ts +21 -0
  10. package/dist/types/browser/navigation-transaction.d.ts +2 -0
  11. package/dist/types/client-urls/server-projection.d.ts +7 -0
  12. package/dist/types/client-urls/types.d.ts +13 -14
  13. package/dist/types/errors.d.ts +5 -0
  14. package/dist/types/loader-store.d.ts +11 -1
  15. package/dist/types/prefetch-markers.d.ts +13 -4
  16. package/dist/types/route-definition/helpers-types.d.ts +14 -10
  17. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
  18. package/dist/types/server/context.d.ts +6 -5
  19. package/dist/types/testing/render-route.d.ts +21 -1
  20. package/dist/types/testing/serve-shell-request.d.ts +7 -7
  21. package/dist/types/testing/vitest-stubs/plugin-rsc-browser.d.ts +33 -0
  22. package/dist/types/types/loader-types.d.ts +10 -8
  23. package/dist/types/urls/path-helper-types.d.ts +9 -8
  24. package/dist/vite/index.js +30 -13
  25. package/package.json +1 -1
  26. package/skills/app-structure/large-app.md +3 -0
  27. package/skills/client-urls/SKILL.md +13 -10
  28. package/skills/hooks/data.md +5 -0
  29. package/skills/hooks/state.md +13 -11
  30. package/skills/intercept/SKILL.md +8 -4
  31. package/skills/links/SKILL.md +15 -12
  32. package/skills/loader/SKILL.md +52 -39
  33. package/skills/ppr/SKILL.md +1 -1
  34. package/skills/route/SKILL.md +15 -9
  35. package/skills/testing/SKILL.md +1 -0
  36. package/skills/testing/client-components.md +67 -19
  37. package/skills/testing/serve-shell-request.md +19 -7
  38. package/skills/ux-architecture/SKILL.md +16 -11
  39. package/src/browser/history-state.ts +48 -0
  40. package/src/browser/navigation-bridge.ts +12 -1
  41. package/src/browser/navigation-transaction.ts +14 -6
  42. package/src/browser/partial-update.ts +12 -4
  43. package/src/browser/prefetch/fetch.ts +3 -3
  44. package/src/client-urls/client-urls.ts +11 -7
  45. package/src/client-urls/server-projection.ts +21 -3
  46. package/src/client-urls/types.ts +13 -14
  47. package/src/errors.ts +17 -0
  48. package/src/loader-store.ts +41 -4
  49. package/src/prefetch-markers.ts +18 -4
  50. package/src/route-definition/dsl-helpers.ts +13 -6
  51. package/src/route-definition/helpers-types.ts +14 -10
  52. package/src/router/loader-resolution.ts +4 -4
  53. package/src/router/match-api.ts +1 -3
  54. package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
  55. package/src/rsc/rsc-rendering.ts +1 -1
  56. package/src/server/context.ts +6 -5
  57. package/src/testing/render-route.tsx +122 -7
  58. package/src/testing/serve-shell-request.ts +7 -7
  59. package/src/testing/vitest-stubs/plugin-rsc-browser.ts +61 -0
  60. package/src/testing/vitest.ts +5 -0
  61. package/src/types/loader-types.ts +10 -8
  62. package/src/urls/path-helper-types.ts +9 -8
  63. package/src/use-loader.tsx +29 -7
@@ -66,13 +66,13 @@ export interface ServeShellRequestOptions<TEnv = any> {
66
66
  * `result.replayStatus`.
67
67
  *
68
68
  * `prefetch: true` sends the speculative kind a viewport or render
69
- * `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false`
70
- * loaders and `loading()` entries are deferred: `result.readDeferred()`
71
- * lists them. A hover's or `prefetch()`'s intent prefetch defers nothing;
72
- * serve it with `prefetch: true` and `headers: { "X-Rango-Prefetch":
73
- * "intent" }`, which `prefetch: true` leaves as given. `fill: true`
74
- * sends the request the browser follows an adopted prefetch with
75
- * (`_rsc_fill`): pass the ids the prefetch delivered, without the deferred
69
+ * `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so loaders and
70
+ * `loading()` entries flagged `prefetch: false` or `"intent"` are deferred:
71
+ * `result.readDeferred()` lists them. A hover's intent prefetch defers
72
+ * `false` only; serve it with `prefetch: true` and `headers: {
73
+ * "X-Rango-Prefetch": "intent" }`, which `prefetch: true` leaves as given.
74
+ * `fill: true` sends the request the browser follows an adopted prefetch
75
+ * with (`_rsc_fill`): pass the ids the prefetch delivered, without the deferred
76
76
  * ones, as `segments`, and the same URL as `from`. `interceptSource` sends
77
77
  * the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends
78
78
  * from an open modal and with the fill of an intercept, whose `from` is
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Stub for `@vitejs/plugin-rsc/browser`, shipped so consumers do not have to
3
+ * write a per-file `vi.mock(...)`. The real entry imports the
4
+ * `virtual:vite-rsc/client-references` Vite virtual at the top level, which a
5
+ * bare Vitest process cannot resolve.
6
+ *
7
+ * The router reaches this module from `useLoader().load()` and a
8
+ * `refreshGroup` refresh (use-loader.tsx imports deps/browser.ts lazily).
9
+ * renderRoute answers those requests (render-route.tsx), and a DOM test has
10
+ * no Flight server to encode an answer: renderRoute's seeds are client values
11
+ * already. So the answer travels with its Response object, and
12
+ * `createFromFetch` hands it back unchanged, as a decoded payload. Any other
13
+ * response is rejected. The remaining exports exist for the re-export in
14
+ * deps/browser.ts; navigations, actions and the document are not decoded
15
+ * here.
16
+ */
17
+ /**
18
+ * A loader endpoint Response whose decoded payload is `payload`
19
+ * (`{ loaderResult }` or `{ loaderResult: null, loaderError }`, as
20
+ * rsc/loader-fetch.ts answers).
21
+ */
22
+ export declare function loaderAnswerResponse(payload: {
23
+ loaderResult: unknown;
24
+ loaderError?: {
25
+ message: string;
26
+ name: string;
27
+ };
28
+ }): Response;
29
+ export declare function createFromFetch<T>(response: Promise<Response> | Response): Promise<T>;
30
+ export declare const createFromReadableStream: () => Promise<never>;
31
+ export declare const encodeReply: () => Promise<never>;
32
+ export declare const setServerCallback: () => void;
33
+ export declare const createTemporaryReferenceSet: () => unknown;
@@ -7,6 +7,7 @@ import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
7
7
  import type { UseItems, LoaderUseItem } from "../route-types.js";
8
8
  import type { DefaultEnv, DefaultReverseRouteMap, DefaultVars } from "./global-namespace.js";
9
9
  import type { RequestScope } from "./request-scope.js";
10
+ import type { PrefetchOption } from "../prefetch-markers.js";
10
11
  /**
11
12
  * Context passed to loader functions during execution
12
13
  *
@@ -182,13 +183,14 @@ export type LoaderFn<T, TParams = Record<string, string | undefined>, TEnv = Def
182
183
  export type LoaderOptions = {
183
184
  ssr?: boolean;
184
185
  /**
185
- * `prefetch: false` keeps this loader out of speculative prefetches. A
186
- * viewport or render `<Link>` prefetch (a partial request with
187
- * `X-Rango-Prefetch: 1`) does not execute it; the payload marks it
188
- * deferred. A hover prefetch and `useRouter().prefetch()`
189
- * (`X-Rango-Prefetch: intent`) count as intent to click and run it, so a
190
- * click after a hover needs no follow-up request. A document request and a
191
- * navigation with no prefetch to adopt run it as usual.
186
+ * `prefetch: false` keeps this loader out of every `<Link>` prefetch: a
187
+ * prefetch (a partial request with `X-Rango-Prefetch`) does not execute
188
+ * it; the payload marks it deferred. `prefetch: "intent"` keeps it out of
189
+ * speculative prefetches only: a viewport or render prefetch
190
+ * (`X-Rango-Prefetch: 1`) defers it, and a hover prefetch or
191
+ * `useRouter().prefetch()` (`X-Rango-Prefetch: intent`) runs it, so a click
192
+ * after a hover needs no follow-up request for it. A document request and
193
+ * a navigation with no prefetch to adopt run it as usual.
192
194
  *
193
195
  * A deferred loader never blocks the navigation that adopts the prefetch:
194
196
  * it behaves like a loader that is still streaming. Its own `<Suspense>`
@@ -213,7 +215,7 @@ export type LoaderOptions = {
213
215
  * request only deferred work and middleware run, so values a held handler
214
216
  * would have set with `ctx.set()` are not visible.
215
217
  */
216
- prefetch?: boolean;
218
+ prefetch?: PrefetchOption;
217
219
  };
218
220
  /**
219
221
  * Options for fetchable loaders
@@ -8,6 +8,7 @@ import type { ResponseHandler, ResponseHandlerContext, TextResponseHandler } fro
8
8
  import type { UnnamedRoute, LocalOnlyInclude, PathOptions, UrlPatterns, IncludeOptions } from "./pattern-types.js";
9
9
  import type { ExtractRoutes, ExtractResponses } from "./type-extraction.js";
10
10
  import type { ClientUrlPatterns } from "../client-urls/types.js";
11
+ import type { PrefetchOption } from "../prefetch-markers.js";
11
12
  /**
12
13
  * Base path function signature for defining routes with URL patterns.
13
14
  */
@@ -161,10 +162,10 @@ export type PathHelpers<TEnv> = {
161
162
  * freezes into the shell (nested promises stay live holes) — the pre-flush
162
163
  * promise applied to the prelude.
163
164
  *
164
- * Pass `{ prefetch: false }` to keep the loader out of speculative
165
- * prefetches (viewport, render): such a prefetch skips it and the click
166
- * that adopts it fetches it in one follow-up request; a hover prefetch runs
167
- * it (see {@link LoaderOptions}). Only on a segment the
165
+ * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
166
+ * prefetch skips it and the click that adopts it fetches it in one
167
+ * follow-up request. `{ prefetch: "intent" }` defers it in viewport and
168
+ * render prefetches only (see {@link LoaderOptions}). Only on a segment the
168
169
  * client does not have yet: a loader the client already holds runs in the
169
170
  * prefetch when it revalidates.
170
171
  */
@@ -173,9 +174,9 @@ export type PathHelpers<TEnv> = {
173
174
  * Attach a loading component to the current route/layout.
174
175
  *
175
176
  * `{ ssr: false }` skips the fallback on document requests. `{ prefetch:
176
- * false }` keeps what the fallback covers out of viewport and render
177
- * prefetches (a hover prefetch runs it), when this entry's segment is new
178
- * to the client: the entry's handler, its loaders,
177
+ * false }` keeps what the fallback covers out of prefetches (`"intent"`:
178
+ * viewport and render prefetches only, see {@link LoaderOptions}), when this
179
+ * entry's segment is new to the client: the entry's handler, its loaders,
179
180
  * its orphan layouts and parallel slots and, for a layout, every deeper
180
181
  * entry. The click that adopts the prefetch shows the fallback and fetches
181
182
  * them in one follow-up request, which reads cache() and "use cache" as
@@ -189,7 +190,7 @@ export type PathHelpers<TEnv> = {
189
190
  */
190
191
  loading: (component: ReactNode | (() => ReactNode), options?: {
191
192
  ssr?: boolean;
192
- prefetch?: boolean;
193
+ prefetch?: PrefetchOption;
193
194
  }) => LoadingItem;
194
195
  /**
195
196
  * Attach an error boundary to catch errors in this segment
@@ -111,6 +111,17 @@ var init_debug = __esm({
111
111
  }
112
112
  });
113
113
 
114
+ // src/prefetch-markers.ts
115
+ var PREFETCH_HEADER, PREFETCH_INTENT, PREFETCH_FAILED_ROW;
116
+ var init_prefetch_markers = __esm({
117
+ "src/prefetch-markers.ts"() {
118
+ "use strict";
119
+ PREFETCH_HEADER = "X-Rango-Prefetch";
120
+ PREFETCH_INTENT = "intent";
121
+ PREFETCH_FAILED_ROW = ":H_0\n";
122
+ }
123
+ });
124
+
114
125
  // src/internal-debug.ts
115
126
  var INTERNAL_RANGO_DEBUG;
116
127
  var init_internal_debug = __esm({
@@ -725,17 +736,6 @@ var init_validate_redirect_origin = __esm({
725
736
  }
726
737
  });
727
738
 
728
- // src/prefetch-markers.ts
729
- var PREFETCH_HEADER, PREFETCH_INTENT, PREFETCH_FAILED_ROW;
730
- var init_prefetch_markers = __esm({
731
- "src/prefetch-markers.ts"() {
732
- "use strict";
733
- PREFETCH_HEADER = "X-Rango-Prefetch";
734
- PREFETCH_INTENT = "intent";
735
- PREFETCH_FAILED_ROW = ":H_0\n";
736
- }
737
- });
738
-
739
739
  // src/browser/response-adapter.ts
740
740
  function isForeignRouterId(response, expectedId) {
741
741
  const got = response.headers.get("X-RSC-Router-Id");
@@ -4594,6 +4594,9 @@ import * as React4 from "react";
4594
4594
  // src/server/context.ts
4595
4595
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
4596
4596
 
4597
+ // src/errors.ts
4598
+ init_prefetch_markers();
4599
+
4597
4600
  // src/cache/cache-key-utils.ts
4598
4601
  function routerKeyPrefix(routerId2) {
4599
4602
  return `${encodeURIComponent(routerId2)}@`;
@@ -5203,12 +5206,15 @@ import {
5203
5206
  } from "react";
5204
5207
 
5205
5208
  // src/loader-store.ts
5209
+ var NO_ROUTE_DATA = /* @__PURE__ */ new Set();
5206
5210
  var EMPTY_SNAPSHOT = Object.freeze({
5207
5211
  value: void 0,
5208
5212
  hasValue: false,
5209
5213
  error: null,
5210
5214
  isLoading: false,
5211
- requestId: 0
5215
+ requestId: 0,
5216
+ valueFor: NO_ROUTE_DATA,
5217
+ requestFor: NO_ROUTE_DATA
5212
5218
  });
5213
5219
 
5214
5220
  // src/router/route-trie-builder.ts
@@ -5360,6 +5366,9 @@ function insertSegments(node, segments, index, leafBase, paramNames) {
5360
5366
  }
5361
5367
  }
5362
5368
 
5369
+ // src/client-urls/client-urls.ts
5370
+ init_prefetch_markers();
5371
+
5363
5372
  // src/browser/react/use-navigation.ts
5364
5373
  import { useContext as useContext9, useEffect as useEffect7, useRef as useRef5 } from "react";
5365
5374
 
@@ -5921,6 +5930,9 @@ import { jsx as jsx9 } from "react/jsx-runtime";
5921
5930
  // src/server/root-layout.tsx
5922
5931
  import { Fragment as Fragment3, jsx as jsx10 } from "react/jsx-runtime";
5923
5932
 
5933
+ // src/route-definition/dsl-helpers.ts
5934
+ init_prefetch_markers();
5935
+
5924
5936
  // src/prerender.ts
5925
5937
  var PRERENDER_PASSTHROUGH = Object.freeze({
5926
5938
  __brand: "prerenderPassthrough"
@@ -6315,8 +6327,13 @@ function serializeLoaderOptions(loaders, owner, options) {
6315
6327
  loaders,
6316
6328
  ({ prefetch }) => prefetch === false
6317
6329
  );
6330
+ const intentLoaderIndices = indicesWhere(
6331
+ loaders,
6332
+ ({ prefetch }) => prefetch === "intent"
6333
+ );
6318
6334
  return {
6319
6335
  ...deferrableLoaderIndices.length > 0 ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) } : {},
6336
+ ...intentLoaderIndices.length > 0 ? { intentLoaderIndices: Object.freeze(intentLoaderIndices) } : {},
6320
6337
  ...loaderRevalidate.some((keys) => keys.length > 0) ? {
6321
6338
  loaderRevalidate: Object.freeze(
6322
6339
  loaderRevalidate.map((keys) => Object.freeze(keys))
@@ -7327,7 +7344,7 @@ function resolveVersionsFrom(table, routerId2) {
7327
7344
  // package.json
7328
7345
  var package_default = {
7329
7346
  name: "@rangojs/router",
7330
- version: "0.33.1",
7347
+ version: "0.34.0",
7331
7348
  description: "Django-inspired RSC router with composable URL patterns",
7332
7349
  keywords: [
7333
7350
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -706,6 +706,9 @@ Rules of thumb:
706
706
  never become clicks: activity feeds, analytics panels, reviews below the
707
707
  fold, a list behind a tab.
708
708
  - Do not flag what the likely click needs first: the page's main content.
709
+ - Flag a loader worth starting on hover `prefetch: "intent"` instead: viewport
710
+ and render prefetches skip it, and a hover or `useRouter().prefetch()` runs
711
+ it, so that click needs no fill for it.
709
712
  - Read a flagged loader inside its own `<Suspense>`; with no boundary, React
710
713
  keeps the page being left on screen until the fill answers.
711
714
  - A same-route click (`/app/projects/1` to `/app/projects/2`) prefetches a
@@ -132,7 +132,7 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
132
132
  | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
133
133
  | `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see `/ppr` → The loader lane rule); no response variants. Use callback may contain `loader()`, `loading()`, `transition()` |
134
134
  | `layout()` | `layout(Component, () => [...])` — children are required and must contain at least one `path()`; may also hold `loader()`, `loading()`, `intercept()`, nested `layout()` |
135
- | `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false }, use?)` — see below |
135
+ | `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false \| "intent" }, use?)` — see below |
136
136
  | `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
137
137
  | `revalidate()` | Valid **inside a loader() use callback only**; runs on the server (the build hoists it out of the `"use client"` module) |
138
138
  | `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
@@ -623,25 +623,27 @@ shell, such as a `useActionState` form (React suspends it once to encode its
623
623
  action), is in the first flush too: the router gives React up to three more
624
624
  turns before it, and none when the document is already complete.
625
625
 
626
- ## `prefetch: false` — keep a loader out of speculative prefetches
626
+ ## `prefetch: false` — keep a loader out of prefetches
627
627
 
628
628
  A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
629
- and a viewport or render prefetch skips it. A hover prefetch, and
630
- `useRouter().prefetch()`, is intent to click: it runs the loader, and the
631
- click sends no fill (`/loader` → "`prefetch: false`").
629
+ `prefetch: false` and every prefetch skips it. Flag it `prefetch: "intent"`
630
+ and only a viewport or render prefetch skips it: a hover prefetch, and
631
+ `useRouter().prefetch()`, is intent to click, runs it, and the click sends
632
+ no fill for it (`/loader` → "`prefetch: false`").
632
633
 
633
634
  ```tsx
634
635
  path("/product/:slug", ProductPage, { name: "product" }, () => [
635
636
  loader(PriceLoader),
636
- loader(ReviewsLoader, { prefetch: false }), // kept out of viewport prefetches
637
+ loader(RelatedLoader, { prefetch: "intent" }), // kept out of viewport prefetches
638
+ loader(ReviewsLoader, { prefetch: false }), // kept out of every prefetch
637
639
  loading(<ProductSkeleton />),
638
640
  ]),
639
641
  ```
640
642
 
641
643
  A click inside the group still shows the destination at once. With a
642
- viewport or render prefetch received, the URL and the other loaders' data move with the click
644
+ prefetch received, the URL and the other loaders' data move with the click
643
645
  too, `useOutlet().pending` ends with that commit, and one follow-up request
644
- (the fill) runs only the flagged loaders. Read the flagged loader inside its
646
+ (the fill) runs only the loaders it skipped. Read the flagged loader inside its
645
647
  own `<Suspense>` to show the page and that fallback at once; with no boundary
646
648
  React holds the page being left until the fill answers, as it would with no
647
649
  prefetch. Same rules as on a server route (`/loader` → "`prefetch: false`"):
@@ -660,8 +662,9 @@ rest of the destination; otherwise it is held. A same-route click
660
662
  - `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
661
663
  on a server layout above the group defers nothing for the group's routes.
662
664
  - On an `intercept()` loader the flag works as in `urls()`: an in-group
663
- viewport or render prefetch of the intercept's target defers it, and the
664
- click opens the modal at once with one fill for it; a hover's runs it.
665
+ prefetch of the intercept's target defers it, and the click opens the
666
+ modal at once with one fill for it; a hover's runs a loader marked
667
+ `"intent"`.
665
668
  - With `ssr: false` on a `ppr` group route the flag works as on a server
666
669
  route: deferred unless the prefetch serves the loader from a pin of a
667
670
  document-captured shell (`/ppr` → "`prefetch: false` on a `ppr` route").
@@ -200,6 +200,11 @@ shared value lives as long as at least one component using that key is mounted.
200
200
  A persistent component (e.g. a header) keeps the value across navigations; a
201
201
  route-scoped component's value is reclaimed when it unmounts. Registered-loader
202
202
  reads (keyed or not) reset on navigation from fresh route data, as before.
203
+ A refreshed value belongs to the route data of the readers on screen when
204
+ `load()` was called: a reader that mounts on another page or on a new visit,
205
+ or that a navigation brings new route data, shows that route data from its
206
+ first frame, and a `load()` still in flight when you leave lands on no other
207
+ page; Back onto the refreshed page shows the refresh again.
203
208
 
204
209
  **Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
205
210
 
@@ -96,16 +96,17 @@ reads the entry on screen, not the one `history.state` already holds.
96
96
  Back/forward and state the server sets on a navigation
97
97
  (`ctx.setLocationState()`, `redirect(url, { state })`) follow the same rule.
98
98
 
99
- | What changes the state | A reader gets it |
100
- | ------------------------------------------------------------ | ---------------------------------------------------------------- |
101
- | `<Link state>`, `router.push()` / `router.replace()` | in the commit that shows the destination |
102
- | back/forward | in the commit that restores the entry (cached or refetched) |
103
- | the server, on a navigation (`setLocationState`, a redirect) | in the commit that shows the destination |
104
- | the server, on a document load (a page load, a no-JS post) | once the page has hydrated, as stored state |
105
- | a navigation the server answers with nothing to re-render | with the new location: the page on screen is the entry's |
106
- | `router.push(url, { revalidate: false })` (no server fetch) | with the new location: the page does not change |
107
- | a server action's `setLocationState` (no navigation) | when the action's response arrives, added to what readers show |
108
- | `Def.write()` / `Def.delete()` | when the entry is next restored: back/forward to it, or a reload |
99
+ | What changes the state | A reader gets it |
100
+ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
101
+ | `<Link state>`, `router.push()` / `router.replace()` | in the commit that shows the destination |
102
+ | back/forward | in the commit that restores the entry (cached or refetched) |
103
+ | the server, on a navigation (`setLocationState`, a redirect) | in the commit that shows the destination |
104
+ | the server, on a document load (a page load, a no-JS post) | once the page has hydrated, as stored state |
105
+ | a navigation the server answers with nothing to re-render | with the new location: the page on screen is the entry's |
106
+ | `router.push(url, { revalidate: false })` (no server fetch) | with the new location: the page does not change |
107
+ | a server action's `setLocationState` (no navigation) | when the action's response arrives, added to what readers show |
108
+ | `router.refresh()`, or another tab's action refreshing this one | in the commit that shows the refreshed page: what readers show, plus what the server set |
109
+ | `Def.write()` / `Def.delete()` | when the entry is next restored: back/forward to it, or a reload |
109
110
 
110
111
  Three consequences:
111
112
 
@@ -260,7 +261,8 @@ and hydrating, so SSR and hydration agree), then clears it from `history.state`
260
261
  `replaceState` in a `useEffect`. Every component reading the same flash
261
262
  definition sees the value, whenever it mounts, until the next navigation or
262
263
  back/forward commits its entry's state: an entry that carries no flash shows
263
- none. A server action's state does not end it. Pressing back/forward will not
264
+ none. A server action's state does not end it, nor a refresh, which keeps it
265
+ on screen without writing it back to the entry. Pressing back/forward will not
264
266
  re-show the flash since it was cleared.
265
267
 
266
268
  Set flash state from the server via `redirect()` with state (from a handler,
@@ -69,6 +69,7 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
69
69
  | Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
70
70
  | Direct URL `/shop/product/abc`, or a reload | Full `<ProductPage />` page |
71
71
  | `router.refresh()`, or another tab's action | Full `<ProductPage />` page, as a reload |
72
+ | Link inside the intercept to another intercepted URL | That intercept, over the same source page |
72
73
  | Back from the open intercept | The intercept closes; the source page stays |
73
74
  | Back/Forward onto the intercept's entry | The intercept over its source page |
74
75
  | The same, uncached, from a page other than the source | Full `<ProductPage />` page, as a document load |
@@ -311,13 +312,16 @@ intercepted: it gets the full page.
311
312
  ### `prefetch: false` on an intercept's loaders
312
313
 
313
314
  `loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
314
- of a viewport or render prefetch of the target from the source page (a hover
315
- prefetch runs it, and its click opens the modal complete with no fill):
315
+ of every prefetch of the target from the source page. `{ prefetch: "intent" }`
316
+ keeps it out of viewport and render prefetches only: a hover prefetch runs
317
+ it, and when every flagged loader of the modal is `"intent"` its click opens
318
+ the modal complete with no fill:
316
319
 
317
320
  ```typescript
318
321
  intercept("@modal", "product", <ProductModal />, () => [
319
322
  loader(ProductLoader),
320
- loader(ReviewsLoader, { prefetch: false }), // skipped in a viewport prefetch
323
+ loader(PhotosLoader, { prefetch: "intent" }), // skipped in a viewport prefetch, runs on hover
324
+ loader(ReviewsLoader, { prefetch: false }), // skipped in every prefetch
321
325
  loading(<ModalSkeleton />),
322
326
  ]);
323
327
  ```
@@ -330,7 +334,7 @@ click shows nothing until its response. The modal's loaders resolve together,
330
334
  so its `loading()` shows until the last one arrives. A handler that reads the
331
335
  flagged loader with `ctx.use()` before its first `await` (or one with no
332
336
  `loading()`) gets it run in the prefetch instead: one run, no fill for it.
333
- `loading(…, { prefetch: false })` inside `intercept()` defers nothing.
337
+ A `prefetch` option on `loading()` inside `intercept()` defers nothing.
334
338
 
335
339
  A prefetch of a route an intercept targets, or one sent from such a route, is
336
340
  stored per source page, whether or not the intercept applies from that page
@@ -619,14 +619,15 @@ 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 speculative (viewport, render) prefetches on
623
- the route, not on the link:
622
+ Keep the expensive part out of prefetches on the route, not on the link:
624
623
 
625
624
  ```tsx
626
- // skip one loader
625
+ // skip one loader in every prefetch
627
626
  loader(ReviewsLoader, { prefetch: false }),
628
627
  // skip what the fallback covers
629
628
  loading(<OrdersSkeleton />, { prefetch: false }),
629
+ // skip it in viewport and render prefetches only: a hover runs it
630
+ loader(RelatedLoader, { prefetch: "intent" }),
630
631
  ```
631
632
 
632
633
  A click that adopts such a prefetch commits what was prefetched, shows the
@@ -635,13 +636,15 @@ is new to the page is deferred: a segment already on screen (the layout of the
635
636
  section you are in, the route itself on `/product/a` to `/product/b`) is
636
637
  prefetched as usual.
637
638
 
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
639
+ A hover prefetch is treated as intent (`X-Rango-Prefetch: intent`, where a
640
+ viewport or render prefetch sends `1`); so is `useRouter().prefetch()`. It
641
+ runs the work marked `prefetch: "intent"` and still skips the work marked
642
+ `prefetch: false`, so on a route whose flags are all `"intent"` its click
643
+ commits complete with no extra request. A hover sends that request only when
644
+ the link has no prefetch cached or in flight: on a viewport link whose
645
+ prefetch has already answered, it sends nothing, and the click fetches the
646
+ deferred part as above. Middleware that skips side effects in prefetches
647
+ should test for the header's presence, not for `=== "1"`. See
645
648
  `/loader` → "`prefetch: false`" and `/route` → "`loading()` options". To stop
646
649
  a link or a whole section from prefetching at all, use the `prefetch` prop or
647
650
  `data-prefetch-scope` above.
@@ -668,8 +671,8 @@ The answer is page-only (`x-rsc-prefetch-scope: source`) when:
668
671
 
669
672
  - An intercept targets the route, or the page that sent the prefetch,
670
673
  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).
674
+ - The answer defers a `prefetch: false` or `prefetch: "intent"` loader or
675
+ `loading()` that the page does not show yet.
673
676
  - A `revalidate()` on the target read `currentUrl` and kept its segment.
674
677
  - A `revalidate()` on the route, or on a segment inside its `path()`, read
675
678
  the page being left and kept the segment through the route's default