@rangojs/router 0.28.0 → 0.29.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 (79) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/browser/action-fence.d.ts +2 -1
  3. package/dist/types/browser/event-controller.d.ts +6 -1
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/use-action.d.ts +7 -2
  6. package/dist/types/browser/react/use-handle.d.ts +19 -16
  7. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  8. package/dist/types/browser/segment-reconciler.d.ts +26 -0
  9. package/dist/types/browser/types.d.ts +22 -4
  10. package/dist/types/browser/wire-protocol.d.ts +1 -0
  11. package/dist/types/cache/tag-invalidation.d.ts +0 -10
  12. package/dist/types/router/error-handling.d.ts +7 -1
  13. package/dist/types/router/match-context.d.ts +2 -0
  14. package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
  15. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  16. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  17. package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
  18. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  19. package/dist/types/router/types.d.ts +1 -1
  20. package/dist/types/server/request-context.d.ts +30 -1
  21. package/dist/types/testing/render-route.d.ts +43 -8
  22. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  23. package/dist/types/types/error-types.d.ts +27 -8
  24. package/dist/types/wire-params.d.ts +5 -0
  25. package/dist/vite/index.js +15 -19
  26. package/package.json +1 -1
  27. package/skills/client-urls/SKILL.md +4 -2
  28. package/skills/hooks/SKILL.md +1 -1
  29. package/skills/hooks/data.md +8 -1
  30. package/skills/hooks/handle-and-actions.md +33 -2
  31. package/skills/hooks/navigation.md +121 -15
  32. package/skills/hooks/state.md +6 -4
  33. package/skills/hooks/urls.md +16 -5
  34. package/skills/intercept/SKILL.md +6 -0
  35. package/skills/observability/SKILL.md +30 -0
  36. package/skills/server-actions/SKILL.md +8 -1
  37. package/skills/testing/client-components.md +25 -18
  38. package/skills/testing/serve-shell-request.md +7 -7
  39. package/skills/view-transitions/SKILL.md +1 -1
  40. package/src/browser/action-fence.ts +2 -1
  41. package/src/browser/event-controller.ts +32 -10
  42. package/src/browser/navigation-bridge.ts +45 -13
  43. package/src/browser/navigation-client.ts +2 -0
  44. package/src/browser/network-error-handler.ts +4 -0
  45. package/src/browser/partial-update.ts +33 -6
  46. package/src/browser/react/use-action.ts +24 -48
  47. package/src/browser/react/use-handle.ts +22 -18
  48. package/src/browser/react/use-link-status.ts +48 -60
  49. package/src/browser/react/use-navigation.ts +92 -41
  50. package/src/browser/segment-reconciler.ts +43 -0
  51. package/src/browser/server-action-bridge.ts +35 -8
  52. package/src/browser/types.ts +23 -4
  53. package/src/browser/wire-protocol.ts +2 -0
  54. package/src/cache/cache-runtime.ts +28 -31
  55. package/src/cache/document-cache.ts +34 -37
  56. package/src/cache/read-through-swr.ts +20 -18
  57. package/src/cache/tag-invalidation.ts +3 -18
  58. package/src/router/error-handling.ts +18 -6
  59. package/src/router/intercept-resolution.ts +9 -4
  60. package/src/router/match-api.ts +1 -0
  61. package/src/router/match-context.ts +3 -0
  62. package/src/router/match-middleware/background-revalidation.ts +5 -4
  63. package/src/router/match-middleware/cache-lookup.ts +5 -3
  64. package/src/router/navigation-snapshot.ts +10 -0
  65. package/src/router/segment-resolution/helpers.ts +49 -13
  66. package/src/router/segment-resolution/loader-cache.ts +4 -3
  67. package/src/router/segment-resolution/resolution-pass.ts +2 -0
  68. package/src/router/segment-resolution/revalidation.ts +57 -28
  69. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  70. package/src/router/types.ts +1 -1
  71. package/src/router.ts +7 -4
  72. package/src/rsc/request-stages.ts +4 -2
  73. package/src/rsc/shell-capture.ts +63 -58
  74. package/src/server/request-context.ts +49 -0
  75. package/src/testing/dispatch.ts +3 -0
  76. package/src/testing/render-route.tsx +199 -13
  77. package/src/testing/serve-shell-request.ts +19 -10
  78. package/src/types/error-types.ts +27 -8
  79. package/src/wire-params.ts +5 -0
@@ -19,6 +19,11 @@ export declare const WIRE: {
19
19
  readonly rewriteTarget: "_rsc_rt";
20
20
  /** A stale request: `true` (router/navigation-snapshot.ts). */
21
21
  readonly stale: "_rsc_stale";
22
+ /**
23
+ * The id of an action's error boundary on screen that a stale request
24
+ * holds: read only beside `stale` (router/navigation-snapshot.ts).
25
+ */
26
+ readonly boundary: "_rsc_boundary";
22
27
  /** A fill request (docs/design/prefetch-false.md): `1`. */
23
28
  readonly fill: "_rsc_fill";
24
29
  /** A loader fetch's loader id (rsc/loader-fetch.ts). */
@@ -298,6 +298,11 @@ var init_wire_params = __esm({
298
298
  rewriteTarget: "_rsc_rt",
299
299
  /** A stale request: `true` (router/navigation-snapshot.ts). */
300
300
  stale: "_rsc_stale",
301
+ /**
302
+ * The id of an action's error boundary on screen that a stale request
303
+ * holds: read only beside `stale` (router/navigation-snapshot.ts).
304
+ */
305
+ boundary: "_rsc_boundary",
301
306
  /** A fill request (docs/design/prefetch-false.md): `1`. */
302
307
  fill: "_rsc_fill",
303
308
  /** A loader fetch's loader id (rsc/loader-fetch.ts). */
@@ -723,6 +728,7 @@ function writePartialParams(url, init) {
723
728
  params.set(WIRE.segments, init.segmentIds.join(","));
724
729
  }
725
730
  if (init.stale) params.set(WIRE.stale, "true");
731
+ if (init.boundary) params.set(WIRE.boundary, init.boundary);
726
732
  if (init.version) params.set(WIRE.version, init.version);
727
733
  if (init.routerId) params.set(WIRE.routerId, init.routerId);
728
734
  if (init.rewriteTarget) params.set(WIRE.rewriteTarget, init.rewriteTarget);
@@ -5168,7 +5174,6 @@ import {
5168
5174
  useState as useState4,
5169
5175
  useEffect as useEffect6,
5170
5176
  useLayoutEffect as useLayoutEffect2,
5171
- startTransition as startTransition2,
5172
5177
  useRef as useRef4
5173
5178
  } from "react";
5174
5179
 
@@ -5194,14 +5199,7 @@ import { useContext as useContext11, useState as useState7, useEffect as useEffe
5194
5199
  var EMPTY_PARAMS = Object.freeze({});
5195
5200
 
5196
5201
  // src/browser/react/use-action.ts
5197
- import {
5198
- useContext as useContext12,
5199
- useState as useState8,
5200
- useEffect as useEffect10,
5201
- useRef as useRef8,
5202
- useOptimistic as useOptimistic2,
5203
- startTransition as startTransition3
5204
- } from "react";
5202
+ import { useContext as useContext12, useState as useState8, useEffect as useEffect10, useRef as useRef8 } from "react";
5205
5203
 
5206
5204
  // src/browser/event-controller.ts
5207
5205
  init_notify_listeners();
@@ -5275,7 +5273,7 @@ import {
5275
5273
  useMemo as useMemo7,
5276
5274
  useRef as useRef11,
5277
5275
  use as use4,
5278
- startTransition as startTransition4
5276
+ startTransition as startTransition2
5279
5277
  } from "react";
5280
5278
 
5281
5279
  // src/root-error-boundary.tsx
@@ -5325,7 +5323,7 @@ import {
5325
5323
  useContext as useContext15,
5326
5324
  useEffect as useEffect15,
5327
5325
  useMemo as useMemo8,
5328
- useRef as useRef13
5326
+ useRef as useRef12
5329
5327
  } from "react";
5330
5328
 
5331
5329
  // src/browser/react/use-link-status.ts
@@ -5333,10 +5331,7 @@ import {
5333
5331
  createContext as createContext7,
5334
5332
  useContext as useContext14,
5335
5333
  useState as useState12,
5336
- useEffect as useEffect14,
5337
- useRef as useRef12,
5338
- useOptimistic as useOptimistic3,
5339
- startTransition as startTransition5
5334
+ useEffect as useEffect14
5340
5335
  } from "react";
5341
5336
  var LinkContext = createContext7(
5342
5337
  null
@@ -5544,7 +5539,7 @@ var Link = forwardRef(function Link2({
5544
5539
  [to, isExternal, ctx?.basename]
5545
5540
  );
5546
5541
  const configuredStrategy = prefetch ?? ctx?.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
5547
- const internalRef = useRef13(null);
5542
+ const internalRef = useRef12(null);
5548
5543
  const setRef = useCallback5(
5549
5544
  (node) => {
5550
5545
  internalRef.current = node;
@@ -5556,7 +5551,7 @@ var Link = forwardRef(function Link2({
5556
5551
  },
5557
5552
  [ref]
5558
5553
  );
5559
- const stateRef = useRef13(state);
5554
+ const stateRef = useRef12(state);
5560
5555
  stateRef.current = state;
5561
5556
  const handleClick = useCallback5(
5562
5557
  (e) => {
@@ -5936,6 +5931,7 @@ var DERIVED_RENDER_BARRIER_DESCRIPTOR = {
5936
5931
  ...RENDER_BARRIER_DESCRIPTOR,
5937
5932
  enumerable: false
5938
5933
  };
5934
+ var backgroundWork = new AsyncLocalStorage6();
5939
5935
 
5940
5936
  // src/route-definition/redirect.ts
5941
5937
  init_redirect_origin();
@@ -5952,7 +5948,7 @@ import {
5952
5948
  } from "react";
5953
5949
 
5954
5950
  // src/client-urls/navigation.ts
5955
- import { startTransition as startTransition6 } from "react";
5951
+ import { startTransition as startTransition3 } from "react";
5956
5952
 
5957
5953
  // src/client-urls/client-root.tsx
5958
5954
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -7146,7 +7142,7 @@ function resolveVersionsFrom(table, routerId2) {
7146
7142
  // package.json
7147
7143
  var package_default = {
7148
7144
  name: "@rangojs/router",
7149
- version: "0.28.0",
7145
+ version: "0.29.0",
7150
7146
  description: "Django-inspired RSC router with composable URL patterns",
7151
7147
  keywords: [
7152
7148
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -734,8 +734,10 @@ a hard load of the target URL renders the full route.
734
734
  values during SSR (live request seeds the SSR store; on ppr routes the
735
735
  capture/resume renders seed the shell key's own sorted search — search is
736
736
  part of shell identity, so static-part reads are legal).
737
- `useNavigation`/`useLinkStatus` report the canonical nav, but a reader
738
- inside optimistically-swapped content (destination WITH `loading()`)
737
+ `useNavigation`/`useLinkStatus` report the canonical nav (while the frame
738
+ shows, `useNavigation()` is `state: "loading"` with `location` the
739
+ committed URL, even though the URL hooks already report the frame's
740
+ destination), but a reader inside optimistically-swapped content (destination WITH `loading()`)
739
741
  unmounts at click — put status readers in surviving chrome. Errors: wrap
740
742
  throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
741
743
  (route-independent lane; `revalidate()` deliberately not consulted).
@@ -86,7 +86,7 @@ the one for your case.
86
86
  | `useHref()` | Mount-aware href | `(path) => string` |
87
87
  | `useMount()` | Current include() mount path | `string` |
88
88
  | `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
89
- | `useNavigation()` | Reactive navigation state | state, location, pendingUrl, isStreaming |
89
+ | `useNavigation()` | Reactive navigation state | state, location, pendingUrl, isNavigating, isStreaming |
90
90
  | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
91
91
  | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
92
92
  | `useLinkStatus()` | Link pending state | { pending } |
@@ -14,7 +14,8 @@ function ProductPrice() {
14
14
 
15
15
  // data: T (guaranteed - throws if not in context)
16
16
  // isLoading: boolean (refetch/load() states + a held navigation replacing
17
- // this data — NOT the initial streamed read)
17
+ // this data — NOT the initial streamed read, and NOT a server action
18
+ // re-running the loader)
18
19
  // error: Error | null
19
20
 
20
21
  return <span>${data.price}</span>;
@@ -30,6 +31,12 @@ component renders, `data` is present. (On document loads a loader registered
30
31
  with `{ ssr: false }` is already settled at first paint, so its
31
32
  reads never suspend there — see `/loader`.)
32
33
 
34
+ **`isLoading` covers navigations and `load()` only.** When a server action
35
+ re-runs the loader (its revalidation), `isLoading` stays `false`: the action's
36
+ own progress is `useAction()`'s `state`, or `useNavigation().state`, which is
37
+ `"loading"` while an action runs. Read those for an action spinner and keep
38
+ `isLoading` for "this data is being replaced by a navigation or refresh".
39
+
33
40
  **A held navigation flags the data it keeps on screen.** When a navigation
34
41
  keeps the current content visible while the loader re-runs (a `transition()`
35
42
  same-route nav, a same-structure search/filter nav), the reader that is still
@@ -50,7 +50,7 @@ page's values over the page being left:
50
50
  | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
51
  | Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. On a `ppr` shell HIT that holds for the handlers' and the `ssr: false` loaders' pushes, which the shell baked; a live-lane loader's push arrives right after hydration (see `/ppr`). |
52
52
  | Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
53
- | Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
53
+ | Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land right after the page's commit: without view transitions in the same task, before paint; with view transitions when the transition's `ready` resolves |
54
54
  | `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
55
55
 
56
56
  You never see a pending value, a blank, or the destination's title over the old
@@ -193,7 +193,34 @@ tracking actions called outside a `<form action={...}>` flow.
193
193
  ### useAction()
194
194
 
195
195
  Track the lifecycle of a server action, wherever on the page it was invoked.
196
- If the same action fires several times, the hook reports the latest call.
196
+ If the same action fires several times, the hook reports the latest call: the
197
+ last one to start.
198
+
199
+ - When the latest call returns before an earlier one, the hook shows its
200
+ result. The earlier call settling afterwards changes nothing the hook
201
+ reports, a result or an error alike. That call still runs to its end: its
202
+ promise resolves or rejects for its caller, its revalidation re-renders the
203
+ page, and an error-boundary response replaces it.
204
+ - A latest call that was aborted reports nothing: `idle`, with no result or
205
+ error. The hook does not fall back to an earlier call. The router aborts
206
+ calls in flight when another call's response is an error boundary, and on
207
+ Back/Forward into or out of an intercept.
208
+ - `useNavigation()` stays `loading` while any call runs.
209
+
210
+ Which calls count as the same action depends on what you pass:
211
+
212
+ - A function, or a full id (`"src/actions/cart.ts#addToCart"`): that action
213
+ only.
214
+ - A string name (`"addToCart"`): every action with that export name, in any
215
+ `"use server"` file. The hook follows the latest call among all of them.
216
+
217
+ The hook tracks one call at a time, not one per argument. Every
218
+ `AddToCartButton` below reads the same `useAction(addToCart)`, so each shows
219
+ the latest call from any button, whatever its `productId`. A reader that
220
+ filters on `payload` (`payload?.[0] === productId`) cannot track an earlier
221
+ call once a later one starts: from then on the hook reports the later call
222
+ only. For per-call state, keep it with the call: `useActionState` or
223
+ `useTransition` in the component that makes it.
197
224
 
198
225
  ```tsx
199
226
  "use client";
@@ -231,6 +258,10 @@ const isLoading = useAction(addToCart, (s) => s.state === "loading");
231
258
  const isAdding = useAction("addToCart", (s) => s.state === "loading");
232
259
  ```
233
260
 
261
+ Each state change commits urgently, so it shows at once, also while React
262
+ holds a navigation's page. It starts no transition of its own, so it never
263
+ takes that page's transition types.
264
+
234
265
  Pass an action imported directly into the client component, or its export
235
266
  name as a string. An action received as a prop from a server component has
236
267
  lost the metadata `useAction` needs and throws; use the string form there.
@@ -12,10 +12,11 @@ function NavIndicator() {
12
12
  const nav = useNavigation();
13
13
 
14
14
  // State properties
15
- nav.state; // 'idle' | 'loading'
16
- nav.isStreaming; // boolean — RSC data is still streaming
17
- nav.location; // Current URL
18
- nav.pendingUrl; // Target URL during navigation (or null)
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. Once the response has arrived a loader
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: for a navigation on its own, `"idle"` commits in the same commit as the
48
- destination's `useSearchParams()`, `usePathname()` and `useParams()`, so a
49
- component that reads both does not see the navigation finished on the URL it
50
- is leaving. A server action, or another navigation started and aborted,
51
- while React holds that page can still bring `"idle"` with the URL being left.
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. `usePathname()`
94
+ can read the path of the page being left on that page's first screen, until
95
+ React finishes the render of the page it held.
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 reads the destination's `location` and URL
64
- hooks (issue #1046).
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
- Like `useHandle`, it commits with the page: while React holds the page being
191
- left, `useSegments()` keeps that page's path and segments.
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 this link's `to` is in flight:
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`.
@@ -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
- remains (issue #1046): a component that first mounts in the page being left,
136
- after the response arrived and while React still holds the destination, reads
137
- its own entry's location state from `useLocationState` and the destination's
138
- URL and params from `usePathname()`, `useSearchParams()` and `useParams()`.
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
 
@@ -125,6 +125,14 @@ click or `router.push()`.
125
125
  | `router.push(url, { revalidate: false })` | at once: nothing is fetched and the page stays |
126
126
  | inside an optimistic `clientUrls()` destination | at the click, for that branch only (see `/client-urls`) |
127
127
 
128
+ **A newer click that fails while a page is held.** Click a link, and while
129
+ React holds the page being left (a `transition()` route, or a loader still
130
+ streaming) click another whose request fails: the root error screen replaces
131
+ the app, and the held page never shows. Under that screen `useNavigation()`
132
+ reads `"idle"` with the first click's `location`, and no hook reader is on
133
+ screen to see it. The hooks are not wrong, there is simply nobody left to read
134
+ them; the error boundary's reset brings the app back.
135
+
128
136
  **The hooks can disagree with the address bar.** On Back/Forward the browser
129
137
  changes the URL and `history.state` before it tells the router. When the
130
138
  entry has to be fetched (it left the client's history cache, or the cache was
@@ -142,11 +150,14 @@ whole wait. A Back/Forward served from the history cache has no wait.
142
150
  `setSearchParams((prev) => ...)` builds on the same location: called during a
143
151
  pending Back/Forward, `prev` is the search of the page on screen.
144
152
 
145
- One gap (issue #1046): a component that first mounts in the page being left
146
- after the navigation's response has arrived, while React still holds the
147
- destination behind a loader or a `transition()`, reads the destination's URL
148
- and params from these hooks. Components that were already mounted are not
149
- affected. It applies to a push and to Back/Forward alike.
153
+ A component that first mounts in the page being left after the navigation's
154
+ response has arrived, while React still holds the destination behind a loader
155
+ or a `transition()`, reads the destination's URL and params from these hooks
156
+ (issue #1046; by design). Every router hook seeds its first value from the
157
+ router's live state, so a menu opened while a click's page is held highlights
158
+ the destination link. Components already on screen keep the values of the
159
+ page on screen until the destination commits, and then every hook agrees. It
160
+ applies to a push and to Back/Forward alike.
150
161
 
151
162
  ### useHref()
152
163
 
@@ -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. An action sent from an intercept
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