@rangojs/router 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +1 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +6 -1
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/use-action.d.ts +10 -4
- package/dist/types/browser/react/use-handle.d.ts +20 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/react/use-route-state.d.ts +45 -0
- package/dist/types/browser/segment-reconciler.d.ts +26 -0
- package/dist/types/browser/types.d.ts +30 -8
- package/dist/types/browser/wire-protocol.d.ts +1 -0
- package/dist/types/cache/tag-invalidation.d.ts +0 -10
- package/dist/types/router/error-handling.d.ts +7 -1
- package/dist/types/router/match-context.d.ts +2 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/server/request-context.d.ts +30 -1
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/error-types.d.ts +27 -8
- package/dist/types/wire-params.d.ts +5 -0
- package/dist/vite/index.js +62 -67
- package/package.json +1 -1
- package/skills/client-urls/SKILL.md +6 -8
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +40 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +17 -5
- package/skills/intercept/SKILL.md +6 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/event-controller.ts +78 -19
- package/src/browser/navigation-bridge.ts +45 -13
- package/src/browser/navigation-client.ts +2 -0
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/react/use-action.ts +27 -50
- package/src/browser/react/use-handle.ts +27 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +76 -51
- package/src/browser/react/use-params.ts +6 -35
- package/src/browser/react/use-pathname.ts +3 -32
- package/src/browser/react/use-route-state.ts +141 -0
- package/src/browser/react/use-search-params.ts +7 -40
- package/src/browser/segment-reconciler.ts +43 -0
- package/src/browser/server-action-bridge.ts +35 -8
- package/src/browser/types.ts +31 -8
- package/src/browser/wire-protocol.ts +2 -0
- package/src/cache/cache-runtime.ts +28 -31
- package/src/cache/document-cache.ts +34 -37
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/tag-invalidation.ts +3 -18
- package/src/router/error-handling.ts +18 -6
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/match-api.ts +1 -0
- package/src/router/match-context.ts +3 -0
- package/src/router/match-middleware/background-revalidation.ts +5 -4
- package/src/router/match-middleware/cache-lookup.ts +5 -3
- package/src/router/navigation-snapshot.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/resolution-pass.ts +2 -0
- package/src/router/segment-resolution/revalidation.ts +57 -28
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/types.ts +1 -1
- package/src/router.ts +7 -4
- package/src/rsc/request-stages.ts +4 -2
- package/src/rsc/shell-capture.ts +63 -58
- package/src/server/request-context.ts +49 -0
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/render-route.tsx +204 -16
- package/src/testing/serve-shell-request.ts +19 -10
- package/src/types/error-types.ts +27 -8
- package/src/wire-params.ts +5 -0
|
@@ -12,10 +12,11 @@ function NavIndicator() {
|
|
|
12
12
|
const nav = useNavigation();
|
|
13
13
|
|
|
14
14
|
// State properties
|
|
15
|
-
nav.state; // 'idle' | 'loading'
|
|
16
|
-
nav.
|
|
17
|
-
nav.
|
|
18
|
-
nav.
|
|
15
|
+
nav.state; // 'idle' | 'loading' — loading during a navigation AND a server action
|
|
16
|
+
nav.isNavigating; // boolean — a navigation is in flight (false during an action)
|
|
17
|
+
nav.isStreaming; // boolean — RSC data is still streaming (navigation or action)
|
|
18
|
+
nav.location; // The page on screen, until the next page shows
|
|
19
|
+
nav.pendingUrl; // The page being fetched (or null)
|
|
19
20
|
|
|
20
21
|
return nav.state === "loading" ? <Spinner /> : null;
|
|
21
22
|
}
|
|
@@ -27,6 +28,22 @@ function IsLoading() {
|
|
|
27
28
|
}
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
#### Navigation versus server action
|
|
32
|
+
|
|
33
|
+
`state` is `"loading"` while a navigation is in flight and also while a server
|
|
34
|
+
action runs. That is deliberate: something on this screen is loading, so a
|
|
35
|
+
spinner or a progress bar has a signal for an action too. When a bar should
|
|
36
|
+
show for navigations only, read `isNavigating`: true from the start of a
|
|
37
|
+
navigation until its commit, false during an action (where `state` is still
|
|
38
|
+
`"loading"`). `isStreaming` covers the action's response as well as a
|
|
39
|
+
navigation's.
|
|
40
|
+
|
|
41
|
+
| Event | `state` | `isNavigating` | `isStreaming` |
|
|
42
|
+
| --------------- | ----------- | -------------- | ---------------------------- |
|
|
43
|
+
| a navigation | `"loading"` | `true` | true until the stream ends |
|
|
44
|
+
| a server action | `"loading"` | `false` | true until the response ends |
|
|
45
|
+
| neither | `"idle"` | `false` | `false` |
|
|
46
|
+
|
|
30
47
|
#### Pending UI, Back/Forward included
|
|
31
48
|
|
|
32
49
|
A link click, `router.push()` and a Back/Forward that has to fetch its entry
|
|
@@ -40,19 +57,49 @@ on screen:
|
|
|
40
57
|
| the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
|
|
41
58
|
| which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
|
|
42
59
|
|
|
43
|
-
`pendingUrl` covers the network wait
|
|
60
|
+
`pendingUrl` covers the network wait, and `useLinkStatus()` covers more: a
|
|
61
|
+
link's `pending` stays `true` after `pendingUrl` turns `null`, while the
|
|
62
|
+
response streams and React holds the page being left, until the destination
|
|
63
|
+
commits (see [useLinkStatus()](#uselinkstatus)). For a spinner on the clicked
|
|
64
|
+
link, read `useLinkStatus()`; `pendingUrl` names the URL only while the
|
|
65
|
+
request is out.
|
|
66
|
+
|
|
67
|
+
Once the response has arrived a loader
|
|
44
68
|
may still be streaming, and React keeps the page being left on screen where a
|
|
45
69
|
navigation holds its content (a same-route change, a `transition()` route,
|
|
46
70
|
with or without a view transition). `state` stays `"loading"` through that
|
|
47
|
-
part:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
while React holds
|
|
71
|
+
part: `"idle"` commits with the destination's `useSearchParams()`,
|
|
72
|
+
`usePathname()` and `useParams()`, so a component that reads both does not
|
|
73
|
+
see the navigation finished on the URL it is leaving. That holds when a
|
|
74
|
+
server action settles, or a newer navigation ends with no page (an external
|
|
75
|
+
redirect, a reload), while React holds the page: `"idle"` waits for the page.
|
|
76
|
+
A newer navigation that fails shows the error screen, and `"idle"` arrives
|
|
77
|
+
with it. A newer click shows at once (`"loading"`, its `pendingUrl`), also
|
|
78
|
+
while an action is in flight. After a server action, `"idle"` arrives with
|
|
79
|
+
the action's revalidated content.
|
|
52
80
|
`isStreaming` can outlast that commit (a loader still streaming below a
|
|
53
81
|
boundary that already showed), so use it for a progress indicator that should
|
|
54
82
|
last until the new page is complete.
|
|
55
83
|
|
|
84
|
+
While React holds a page, every field follows one rule, whatever the reader
|
|
85
|
+
reads (a selector, the full state): work that starts shows at once
|
|
86
|
+
(`"loading"`, `isStreaming` and `isNavigating` turning `true`, a newer click's
|
|
87
|
+
`pendingUrl`), and what settles waits for the page (`"idle"`, `false`, a
|
|
88
|
+
`null` `pendingUrl`). So a progress bar that reads
|
|
89
|
+
`useNavigation((n) => n.isStreaming)` stays up until the held page shows, even
|
|
90
|
+
when the response stream ended earlier, and agrees with every other
|
|
91
|
+
`useNavigation()` reader on the screen. A page that ends the hold urgently (a
|
|
92
|
+
Back to a page without `transition()`, a link with `transition={false}`)
|
|
93
|
+
shows with its own values in every `useNavigation()` reader, and with its URL
|
|
94
|
+
in `usePathname()`, `useParams()`, `useSearchParams()` and `useSegments()`,
|
|
95
|
+
also when it is the URL React was holding.
|
|
96
|
+
|
|
97
|
+
The rule holds for readers mounted before the page was handed to React. A
|
|
98
|
+
reader that mounts while the page is held (a menu opened during the wait)
|
|
99
|
+
never saw the hand-over and reads the live values, the destination's `idle`
|
|
100
|
+
and location, until the page shows. This is by design (#1046): its first
|
|
101
|
+
render has no page of its own to wait for.
|
|
102
|
+
|
|
56
103
|
On Back/Forward the address bar changes at once, before any of this: during
|
|
57
104
|
the wait `window.location` is the entry being fetched, the same URL as
|
|
58
105
|
`pendingUrl`, and `location` is the page on screen. A Back/Forward served
|
|
@@ -60,8 +107,28 @@ from the client's history cache restores its page in one commit and reports
|
|
|
60
107
|
no pending state.
|
|
61
108
|
|
|
62
109
|
`location` moves in that commit too. A component that first mounts while
|
|
63
|
-
React holds the page being left
|
|
64
|
-
|
|
110
|
+
React holds the page being left (a dropdown opened on the old page, a lazy
|
|
111
|
+
component or a `<Suspense>` there that resolves during the hold) reads the
|
|
112
|
+
destination's `state`, `location` and URL hooks, because each hook seeds its
|
|
113
|
+
first value from the router's live state (issue #1046; by design). A menu
|
|
114
|
+
opened while a click's page is held therefore highlights the destination
|
|
115
|
+
link. Components already mounted keep the page on screen's values until the
|
|
116
|
+
destination commits.
|
|
117
|
+
|
|
118
|
+
A second click while React holds the first click's page works the same way:
|
|
119
|
+
`"loading"` and the second click's `pendingUrl` show at once, and `location`
|
|
120
|
+
stays the page on screen. It moves to the first click's page only if that
|
|
121
|
+
page shows before the second one arrives; if the second page replaces it
|
|
122
|
+
first, the first page never shows and its URL is never `location`.
|
|
123
|
+
|
|
124
|
+
**One exception: inside an optimistic `clientUrls()` frame.** When a
|
|
125
|
+
cross-route navigation shows the destination's `loading()` frame at the click,
|
|
126
|
+
`usePathname()`, `useSearchParams()` and `useParams()` report the frame's
|
|
127
|
+
destination (see [`./urls.md`](./urls.md)), but `useNavigation()` reports the
|
|
128
|
+
real navigation: `state` is `"loading"` and `location` is the committed URL,
|
|
129
|
+
the page that is still the router's. So `location` is "which page is
|
|
130
|
+
committed", not always "which page the eye sees", and a status reader should
|
|
131
|
+
sit in chrome that survives the swap (see `/client-urls`).
|
|
65
132
|
|
|
66
133
|
### useRouter()
|
|
67
134
|
|
|
@@ -187,13 +254,17 @@ const segments = useSegments();
|
|
|
187
254
|
const part = segments.path[depth];
|
|
188
255
|
```
|
|
189
256
|
|
|
190
|
-
|
|
191
|
-
|
|
257
|
+
While React holds the page being left, `useSegments()` keeps that page's
|
|
258
|
+
values. The new page's `path` and `location` commit with it, so its effects
|
|
259
|
+
read them; `segmentIds` come with the page's handle data and can land right
|
|
260
|
+
after its commit, as handle values do (see
|
|
261
|
+
[handle-and-actions](./handle-and-actions.md#when-handle-values-change-by-design)).
|
|
192
262
|
|
|
193
263
|
### useLinkStatus()
|
|
194
264
|
|
|
195
265
|
Track pending state inside a Link component. `pending` is `true` while a
|
|
196
|
-
navigation to
|
|
266
|
+
navigation to the link's `to` is in flight, until the page it brings
|
|
267
|
+
commits:
|
|
197
268
|
|
|
198
269
|
```tsx
|
|
199
270
|
"use client";
|
|
@@ -210,3 +281,38 @@ function LoadingIndicator() {
|
|
|
210
281
|
<LoadingIndicator />
|
|
211
282
|
</Link>;
|
|
212
283
|
```
|
|
284
|
+
|
|
285
|
+
The spinner ends in the commit that shows the destination: its `loading()`
|
|
286
|
+
skeleton without `transition()`, or the page itself under `transition()`,
|
|
287
|
+
where React holds the page being left until the new one is ready. Through a
|
|
288
|
+
server redirect, the clicked link stays pending until the target page
|
|
289
|
+
commits. A newer navigation and an abort end it at once, outside the narrow
|
|
290
|
+
case below. A failure ends it with the error: a route's `errorBoundary()` commits like a page, and a
|
|
291
|
+
failure the router shows at its root (a network error, a response it cannot
|
|
292
|
+
read) replaces the page, links included.
|
|
293
|
+
|
|
294
|
+
`pending` follows the same rule as `useNavigation()`: a change commits in the
|
|
295
|
+
lane of the notification that carries it. The click's notification goes out
|
|
296
|
+
a task after the click and commits urgently, so the spinner shows at once,
|
|
297
|
+
and an abort or a newer click ends it the same way. The hook starts no
|
|
298
|
+
transition of its own, so it never takes the page's transition types. A
|
|
299
|
+
fully prefetched click whose page commits before that task shows no pending
|
|
300
|
+
state: its page is already there.
|
|
301
|
+
|
|
302
|
+
One narrow case is a known trade: when another flow's commit (a server
|
|
303
|
+
action's) is handed to React in the same task as the click, before the
|
|
304
|
+
click's notification goes out, the notification commits with that commit.
|
|
305
|
+
If React holds it, the spinner, and `useNavigation()`'s `loading`, show when
|
|
306
|
+
it lands; a newer click or an abort delivered that way ends an earlier
|
|
307
|
+
spinner when it lands too.
|
|
308
|
+
|
|
309
|
+
The two hooks answer different questions. `pending` is this link's
|
|
310
|
+
navigation; `useNavigation()` is the router's, and its `"idle"` waits for any
|
|
311
|
+
page or tree React holds. So a link whose navigation is aborted while React
|
|
312
|
+
holds another flow's commit reads `pending: false` while `state` stays
|
|
313
|
+
`"loading"` until that commit lands: the frame any link that is not the
|
|
314
|
+
destination reads during a navigation.
|
|
315
|
+
|
|
316
|
+
`pending` lasts longer than `useNavigation().pendingUrl`, which turns `null`
|
|
317
|
+
when the response arrives. While the response streams, a link reads `pending`
|
|
318
|
+
and `pendingUrl` is `null`.
|
package/skills/hooks/state.md
CHANGED
|
@@ -132,10 +132,12 @@ The URL hooks follow the same rule for a component that is mounted across the
|
|
|
132
132
|
navigation: on back/forward too, `usePathname()` and `useSearchParams()`
|
|
133
133
|
change with the page, not at the `popstate` event (see
|
|
134
134
|
[`./urls.md`](./urls.md), "When the URL hooks change"). One difference
|
|
135
|
-
|
|
136
|
-
after the response arrived and while React still holds the destination,
|
|
137
|
-
its own entry's location state from `useLocationState` and the
|
|
138
|
-
URL and params from `usePathname()`, `useSearchParams()` and
|
|
135
|
+
is by design (issue #1046): a component that first mounts in the page being
|
|
136
|
+
left, after the response arrived and while React still holds the destination,
|
|
137
|
+
reads its own entry's location state from `useLocationState` and the
|
|
138
|
+
destination's URL and params from `usePathname()`, `useSearchParams()` and
|
|
139
|
+
`useParams()`. Components already on screen keep the page on screen's values
|
|
140
|
+
until the destination commits.
|
|
139
141
|
|
|
140
142
|
### State on router.push() / router.replace()
|
|
141
143
|
|
package/skills/hooks/urls.md
CHANGED
|
@@ -123,8 +123,17 @@ click or `router.push()`.
|
|
|
123
123
|
| Back/Forward, entry fetched again | when the fetched page commits; until then the page being left is on screen |
|
|
124
124
|
| a Back/Forward whose fetch fails | with the error boundary: they report the entry the browser is on |
|
|
125
125
|
| `router.push(url, { revalidate: false })` | at once: nothing is fetched and the page stays |
|
|
126
|
+
| `transition={false}` over a held page | with that page's first screen, also on the URL React was holding |
|
|
126
127
|
| inside an optimistic `clientUrls()` destination | at the click, for that branch only (see `/client-urls`) |
|
|
127
128
|
|
|
129
|
+
**A newer click that fails while a page is held.** Click a link, and while
|
|
130
|
+
React holds the page being left (a `transition()` route, or a loader still
|
|
131
|
+
streaming) click another whose request fails: the root error screen replaces
|
|
132
|
+
the app, and the held page never shows. Under that screen `useNavigation()`
|
|
133
|
+
reads `"idle"` with the first click's `location`, and no hook reader is on
|
|
134
|
+
screen to see it. The hooks are not wrong, there is simply nobody left to read
|
|
135
|
+
them; the error boundary's reset brings the app back.
|
|
136
|
+
|
|
128
137
|
**The hooks can disagree with the address bar.** On Back/Forward the browser
|
|
129
138
|
changes the URL and `history.state` before it tells the router. When the
|
|
130
139
|
entry has to be fetched (it left the client's history cache, or the cache was
|
|
@@ -142,11 +151,14 @@ whole wait. A Back/Forward served from the history cache has no wait.
|
|
|
142
151
|
`setSearchParams((prev) => ...)` builds on the same location: called during a
|
|
143
152
|
pending Back/Forward, `prev` is the search of the page on screen.
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
A component that first mounts in the page being left after the navigation's
|
|
155
|
+
response has arrived, while React still holds the destination behind a loader
|
|
156
|
+
or a `transition()`, reads the destination's URL and params from these hooks
|
|
157
|
+
(issue #1046; by design). Every router hook seeds its first value from the
|
|
158
|
+
router's live state, so a menu opened while a click's page is held highlights
|
|
159
|
+
the destination link. Components already on screen keep the values of the
|
|
160
|
+
page on screen until the destination commits, and then every hook agrees. It
|
|
161
|
+
applies to a push and to Back/Forward alike.
|
|
150
162
|
|
|
151
163
|
### useHref()
|
|
152
164
|
|
|
@@ -68,6 +68,7 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
68
68
|
| ----------------------------------------------------- | ---------------------------------------------------- |
|
|
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
|
+
| `router.refresh()`, or another tab's action | Full `<ProductPage />` page, as a reload |
|
|
71
72
|
| Back from the open intercept | The intercept closes; the source page stays |
|
|
72
73
|
| Back/Forward onto the intercept's entry | The intercept over its source page |
|
|
73
74
|
| The same, uncached, from a page other than the source | Full `<ProductPage />` page, as a document load |
|
|
@@ -81,6 +82,11 @@ state like any document load, and the entry is a full page from then on. In
|
|
|
81
82
|
dev, an edit that changes the app version clears the history cache, so the
|
|
82
83
|
next Back/Forward onto an intercept entry takes that path.
|
|
83
84
|
|
|
85
|
+
An intercept is a page in the middle of a navigation. A refresh reloads its
|
|
86
|
+
URL, so `router.refresh()` with the intercept open, and the refresh another
|
|
87
|
+
tab's action triggers, render the full page, as a document load does. The
|
|
88
|
+
entry is a full page from then on.
|
|
89
|
+
|
|
84
90
|
A server action on `/shop` that calls `redirect("/shop/product/abc")` opens the
|
|
85
91
|
intercept too, and so does a link from `/shop` to a route that redirects
|
|
86
92
|
there. The page under it is the browser's copy, as on a link click: it
|
|
@@ -277,6 +277,36 @@ Events include `request.start/end/error`, `loader.start/end/error`,
|
|
|
277
277
|
`handler.error`, `cache.decision`, `revalidation.decision`, `request.timeout`,
|
|
278
278
|
and `request.origin-rejected`.
|
|
279
279
|
|
|
280
|
+
## Errors reported to onError
|
|
281
|
+
|
|
282
|
+
`createRouter({ onError })` sees every server error, including errors from
|
|
283
|
+
work no visitor waits on. A stale `cache()` record, document cache entry, PPR
|
|
284
|
+
shell, `"use cache"` function or cached loader is served stale and refreshed
|
|
285
|
+
beside the request. That refresh's error carries `background: true`, with the
|
|
286
|
+
starting request's `request` and `url`, and `handledByBoundary: false`: the
|
|
287
|
+
failed render is not stored, so no one sees its fallback. The stale entry
|
|
288
|
+
keeps serving.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const router = createRouter({
|
|
292
|
+
onError: ({ error, phase, background, routeKey, duration }) => {
|
|
293
|
+
errorTracker.capture(error, {
|
|
294
|
+
// A failed refresh is not a failed page view.
|
|
295
|
+
level: background ? "warning" : "error",
|
|
296
|
+
tags: { phase, route: routeKey },
|
|
297
|
+
extra: { duration },
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`duration` is the time from the request's start to the error; for a
|
|
304
|
+
`background` error, from the start of the request that started the refresh.
|
|
305
|
+
`segmentId` and `segmentType` name the segment that threw: an intercept's or
|
|
306
|
+
a parallel slot's error reports the slot (`segmentType: "parallel"`), an
|
|
307
|
+
orphan layout's error the orphan layout, even when the owning layout's
|
|
308
|
+
boundary renders the fallback.
|
|
309
|
+
|
|
280
310
|
## Debugging revalidation and stale data
|
|
281
311
|
|
|
282
312
|
When stale UI or unexpected partial renders are the question, use all three
|
|
@@ -569,6 +569,10 @@ A few details:
|
|
|
569
569
|
until a request re-runs them.
|
|
570
570
|
- It holds no segments, so every loader on the page re-runs whatever its
|
|
571
571
|
`revalidate()` says, and the document cache never answers it.
|
|
572
|
+
- If a later action on that page threw into its `errorBoundary()`, the
|
|
573
|
+
revalidation keeps the boundary on screen: everything above it re-runs,
|
|
574
|
+
nothing at or below it. Under the router's default boundary at the root,
|
|
575
|
+
nothing runs. A link from the page re-runs what is at or below the boundary.
|
|
572
576
|
- Going Back or Forward out of an intercept does not abort an action sent from
|
|
573
577
|
the intercept. `useAction()` and your `await` get its real result.
|
|
574
578
|
|
|
@@ -710,7 +714,10 @@ decides, so a cart count shows what the action wrote before it threw. A
|
|
|
710
714
|
route's `errorBoundary()` replaces the route: its handler and loaders do not
|
|
711
715
|
run. A layout's `errorBoundary()` replaces what the layout renders in its
|
|
712
716
|
outlet: the layout's own loaders, slots and orphan layouts revalidate as on a
|
|
713
|
-
success, and nothing inside the outlet runs.
|
|
717
|
+
success, and nothing inside the outlet runs. A link from the page re-runs
|
|
718
|
+
what the boundary replaced and everything under it; under the router's
|
|
719
|
+
default boundary at the root, the whole page. Back to the page later
|
|
720
|
+
restores it and revalidates it, which replaces the boundary with the route. An action sent from an intercept
|
|
714
721
|
gets the boundary alone.
|
|
715
722
|
|
|
716
723
|
```typescript
|
|
@@ -66,9 +66,13 @@ type RenderRouteResult = RenderResult & {
|
|
|
66
66
|
url: string,
|
|
67
67
|
options?: {
|
|
68
68
|
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
69
|
+
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
|
|
69
70
|
state?: HistoryState;
|
|
70
71
|
replace?: boolean;
|
|
71
72
|
transition?: boolean;
|
|
73
|
+
// the response's stream: opens a task after the call, before the
|
|
74
|
+
// commit, and stays open until the promise settles (isStreaming)
|
|
75
|
+
streamUntil?: Promise<unknown>;
|
|
72
76
|
},
|
|
73
77
|
): Promise<void>;
|
|
74
78
|
// re-render the current location, as router.refresh() does:
|
|
@@ -203,13 +207,14 @@ it("lists no product twice while the next page loads", async () => {
|
|
|
203
207
|
});
|
|
204
208
|
```
|
|
205
209
|
|
|
206
|
-
| To bring state to a reader
|
|
207
|
-
|
|
|
208
|
-
| a push / replace, the component's own
|
|
209
|
-
| a push / replace whose loader the test holds
|
|
210
|
-
| back/forward onto
|
|
211
|
-
|
|
|
212
|
-
| a
|
|
210
|
+
| To bring state to a reader | Do |
|
|
211
|
+
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| a push / replace, the component's own | click its `<Link state>` or the control that calls `router.push(url, { state })`, then `waitFor` |
|
|
213
|
+
| a push / replace whose loader the test holds | `router.navigate(url, { state, replace?, loaders: [[Loader, pendingPromise]] })` on a `transition` spec |
|
|
214
|
+
| back/forward onto the mounted entry or one a navigation wrote | `window.history.back()` inside `await act(async () => { ... })`: the entry's page is restored with its state and handle values, as production's history-cache restore is |
|
|
215
|
+
| back/forward onto an entry the test wrote | `history.replaceState(entry, "")`, then `window.dispatchEvent(new Event("popstate"))` inside `act`: readers take the entry as history holds it (also how a `Def.write()` becomes visible) |
|
|
216
|
+
| a reader that mounts while a navigation is pending | hold the navigation as in the row above, then mount the reader inside `await act(async () => { ... })`: it reads the entry on screen |
|
|
217
|
+
| a server action's state, a redirect's, an intercept | e2e |
|
|
213
218
|
|
|
214
219
|
### Definitions with `clearOnReload`
|
|
215
220
|
|
|
@@ -390,17 +395,19 @@ it("dims the held price while the next product streams", async () => {
|
|
|
390
395
|
});
|
|
391
396
|
```
|
|
392
397
|
|
|
393
|
-
| Pending state | Under `renderRoute`
|
|
394
|
-
| ---------------------------------------------------------------------------------------------- |
|
|
395
|
-
| `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.
|
|
396
|
-
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit.
|
|
397
|
-
| 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.
|
|
398
|
-
| `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.
|
|
399
|
-
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e.
|
|
400
|
-
| `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
|
|
401
|
-
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e.
|
|
402
|
-
| `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.
|
|
403
|
-
|
|
|
398
|
+
| Pending state | Under `renderRoute` |
|
|
399
|
+
| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
400
|
+
| `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. |
|
|
401
|
+
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
|
|
402
|
+
| 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. |
|
|
403
|
+
| `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. |
|
|
404
|
+
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
|
|
405
|
+
| `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. |
|
|
406
|
+
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
|
|
407
|
+
| `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. |
|
|
408
|
+
| `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 () => ...)`. |
|
|
409
|
+
| 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. |
|
|
410
|
+
| 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. |
|
|
404
411
|
|
|
405
412
|
- 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.
|
|
406
413
|
- 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.)
|
|
@@ -20,13 +20,13 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
|
|
|
20
20
|
|
|
21
21
|
### Options — `ServeShellRequestOptions`
|
|
22
22
|
|
|
23
|
-
| Field | Type
|
|
24
|
-
| ------------ |
|
|
25
|
-
| `cacheStore` | `SegmentCacheStore`
|
|
26
|
-
| `env` | `TEnv`
|
|
27
|
-
| `headers` | `HeadersInit`
|
|
28
|
-
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. |
|
|
29
|
-
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }`
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
|
|
26
|
+
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
|
|
27
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
|
|
28
|
+
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error segment of a thrown action's boundary the page at `from` shows (`_rsc_boundary`), as the background revalidation an earlier action owes that page sends it, with `stale: true` and no `segments`: the response renders every segment above the boundary and nothing at or below it, claims the boundary in `matched` and sends no segment for it. The server reads it only beside `stale`. |
|
|
29
|
+
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
|
|
30
30
|
|
|
31
31
|
### Returns — `ServeShellRequestResult`
|
|
32
32
|
|
|
@@ -154,7 +154,7 @@ Every commit the router runs in a transition adds its `transition({ when })` kin
|
|
|
154
154
|
- Tagging alone animates nothing: React starts a view transition only where a `<ViewTransition>` resolves a class other than `"none"` for the commit's types.
|
|
155
155
|
- A commit outside a transition carries no type: a cold click that mounts new segments, a gated-off commit (`when` returned `false`), and a back/forward restored from the history cache on a route without `transition()` (it commits inside the `popstate` event). Under `transition()` that restore carries `"pop"` and `"navigation-back"`.
|
|
156
156
|
- Under `transition()`, a stale entry's revalidation carries `"revalidate"` and `"action"`. An `action`-keyed map that must skip it also keys `revalidate: "none"` (`"none"` wins over any other match).
|
|
157
|
-
- The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `
|
|
157
|
+
- The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useSegments()` commits with the page and `useHandle()` right after it, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. `useAction()` values commit urgently. `useLinkStatus()` commits `pending` urgently and its end with the page, and a newer navigation or an abort ends it urgently, outside a held commit; neither starts a transition. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
|
|
158
158
|
|
|
159
159
|
### Back and Forward
|
|
160
160
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The drop runs the batch's follow-up: when an action of the batch did not
|
|
22
22
|
* land on the page on screen, the action bridge refetches that page once,
|
|
23
|
-
* whole
|
|
23
|
+
* whole (or whole above an action's error boundary on screen, which it
|
|
24
|
+
* holds), after the navigation the user started, unless an intercept is on
|
|
24
25
|
* screen (server-action-bridge.ts followUp; the rules are R1-R7 in
|
|
25
26
|
* docs/internal/execution-model.md).
|
|
26
27
|
*
|