@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.
- package/dist/bin/{chunk-TTXC2U3G.js → chunk-4HHFQSDC.js} +10 -2
- package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
- package/dist/bin/{client-urls-projection-DQLWPDAT.js → client-urls-projection-4Q5EQDAL.js} +2 -2
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
- package/dist/bin/{runtime-discovery-3HH6BP3K.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
- package/dist/bin/{temp-rsc-server-JALTIWLT.js → temp-rsc-server-EUNKRFJD.js} +3 -3
- package/dist/testing/vitest.js +5 -0
- package/dist/types/browser/history-state.d.ts +21 -0
- package/dist/types/browser/navigation-transaction.d.ts +2 -0
- package/dist/types/client-urls/server-projection.d.ts +7 -0
- package/dist/types/client-urls/types.d.ts +13 -14
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/loader-store.d.ts +11 -1
- package/dist/types/prefetch-markers.d.ts +13 -4
- package/dist/types/route-definition/helpers-types.d.ts +14 -10
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
- package/dist/types/server/context.d.ts +6 -5
- package/dist/types/testing/render-route.d.ts +21 -1
- package/dist/types/testing/serve-shell-request.d.ts +7 -7
- package/dist/types/testing/vitest-stubs/plugin-rsc-browser.d.ts +33 -0
- package/dist/types/types/loader-types.d.ts +10 -8
- package/dist/types/urls/path-helper-types.d.ts +9 -8
- package/dist/vite/index.js +30 -13
- package/package.json +1 -1
- package/skills/app-structure/large-app.md +3 -0
- package/skills/client-urls/SKILL.md +13 -10
- package/skills/hooks/data.md +5 -0
- package/skills/hooks/state.md +13 -11
- package/skills/intercept/SKILL.md +8 -4
- package/skills/links/SKILL.md +15 -12
- package/skills/loader/SKILL.md +52 -39
- package/skills/ppr/SKILL.md +1 -1
- package/skills/route/SKILL.md +15 -9
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +67 -19
- package/skills/testing/serve-shell-request.md +19 -7
- package/skills/ux-architecture/SKILL.md +16 -11
- package/src/browser/history-state.ts +48 -0
- package/src/browser/navigation-bridge.ts +12 -1
- package/src/browser/navigation-transaction.ts +14 -6
- package/src/browser/partial-update.ts +12 -4
- package/src/browser/prefetch/fetch.ts +3 -3
- package/src/client-urls/client-urls.ts +11 -7
- package/src/client-urls/server-projection.ts +21 -3
- package/src/client-urls/types.ts +13 -14
- package/src/errors.ts +17 -0
- package/src/loader-store.ts +41 -4
- package/src/prefetch-markers.ts +18 -4
- package/src/route-definition/dsl-helpers.ts +13 -6
- package/src/route-definition/helpers-types.ts +14 -10
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/match-api.ts +1 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
- package/src/rsc/rsc-rendering.ts +1 -1
- package/src/server/context.ts +6 -5
- package/src/testing/render-route.tsx +122 -7
- package/src/testing/serve-shell-request.ts +7 -7
- package/src/testing/vitest-stubs/plugin-rsc-browser.ts +61 -0
- package/src/testing/vitest.ts +5 -0
- package/src/types/loader-types.ts +10 -8
- package/src/urls/path-helper-types.ts +9 -8
- 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
|
|
70
|
-
*
|
|
71
|
-
* lists them. A hover's
|
|
72
|
-
* serve it with `prefetch: true` and `headers: {
|
|
73
|
-
* "intent" }`, which `prefetch: true` leaves as given.
|
|
74
|
-
* sends the request the browser follows an adopted prefetch
|
|
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
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* (`X-Rango-Prefetch:
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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?:
|
|
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
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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
|
|
177
|
-
* prefetches
|
|
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?:
|
|
193
|
+
prefetch?: PrefetchOption;
|
|
193
194
|
}) => LoadingItem;
|
|
194
195
|
/**
|
|
195
196
|
* Attach an error boundary to catch errors in this segment
|
package/dist/vite/index.js
CHANGED
|
@@ -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.
|
|
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
|
@@ -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
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
click
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
664
|
-
|
|
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").
|
package/skills/hooks/data.md
CHANGED
|
@@ -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
|
|
package/skills/hooks/state.md
CHANGED
|
@@ -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
|
|
100
|
-
|
|
|
101
|
-
| `<Link state>`, `router.push()` / `router.replace()`
|
|
102
|
-
| back/forward
|
|
103
|
-
| the server, on a navigation (`setLocationState`, a redirect)
|
|
104
|
-
| the server, on a document load (a page load, a no-JS post)
|
|
105
|
-
| a navigation the server answers with nothing to re-render
|
|
106
|
-
| `router.push(url, { revalidate: false })` (no server fetch)
|
|
107
|
-
| a server action's `setLocationState` (no navigation)
|
|
108
|
-
| `
|
|
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
|
|
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
|
|
315
|
-
|
|
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(
|
|
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
|
-
`
|
|
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
|
package/skills/links/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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`
|
|
672
|
-
|
|
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
|