@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.
Files changed (84) 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 +10 -4
  6. package/dist/types/browser/react/use-handle.d.ts +20 -16
  7. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  8. package/dist/types/browser/react/use-route-state.d.ts +45 -0
  9. package/dist/types/browser/segment-reconciler.d.ts +26 -0
  10. package/dist/types/browser/types.d.ts +30 -8
  11. package/dist/types/browser/wire-protocol.d.ts +1 -0
  12. package/dist/types/cache/tag-invalidation.d.ts +0 -10
  13. package/dist/types/router/error-handling.d.ts +7 -1
  14. package/dist/types/router/match-context.d.ts +2 -0
  15. package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
  16. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  17. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  18. package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
  19. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  20. package/dist/types/router/types.d.ts +1 -1
  21. package/dist/types/server/request-context.d.ts +30 -1
  22. package/dist/types/testing/render-route.d.ts +43 -8
  23. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  24. package/dist/types/types/error-types.d.ts +27 -8
  25. package/dist/types/wire-params.d.ts +5 -0
  26. package/dist/vite/index.js +62 -67
  27. package/package.json +1 -1
  28. package/skills/client-urls/SKILL.md +6 -8
  29. package/skills/hooks/SKILL.md +1 -1
  30. package/skills/hooks/data.md +8 -1
  31. package/skills/hooks/handle-and-actions.md +40 -2
  32. package/skills/hooks/navigation.md +121 -15
  33. package/skills/hooks/state.md +6 -4
  34. package/skills/hooks/urls.md +17 -5
  35. package/skills/intercept/SKILL.md +6 -0
  36. package/skills/observability/SKILL.md +30 -0
  37. package/skills/server-actions/SKILL.md +8 -1
  38. package/skills/testing/client-components.md +25 -18
  39. package/skills/testing/serve-shell-request.md +7 -7
  40. package/skills/view-transitions/SKILL.md +1 -1
  41. package/src/browser/action-fence.ts +2 -1
  42. package/src/browser/event-controller.ts +78 -19
  43. package/src/browser/navigation-bridge.ts +45 -13
  44. package/src/browser/navigation-client.ts +2 -0
  45. package/src/browser/network-error-handler.ts +4 -0
  46. package/src/browser/partial-update.ts +33 -6
  47. package/src/browser/react/use-action.ts +27 -50
  48. package/src/browser/react/use-handle.ts +27 -18
  49. package/src/browser/react/use-link-status.ts +48 -60
  50. package/src/browser/react/use-navigation.ts +76 -51
  51. package/src/browser/react/use-params.ts +6 -35
  52. package/src/browser/react/use-pathname.ts +3 -32
  53. package/src/browser/react/use-route-state.ts +141 -0
  54. package/src/browser/react/use-search-params.ts +7 -40
  55. package/src/browser/segment-reconciler.ts +43 -0
  56. package/src/browser/server-action-bridge.ts +35 -8
  57. package/src/browser/types.ts +31 -8
  58. package/src/browser/wire-protocol.ts +2 -0
  59. package/src/cache/cache-runtime.ts +28 -31
  60. package/src/cache/document-cache.ts +34 -37
  61. package/src/cache/read-through-swr.ts +20 -18
  62. package/src/cache/tag-invalidation.ts +3 -18
  63. package/src/router/error-handling.ts +18 -6
  64. package/src/router/intercept-resolution.ts +9 -4
  65. package/src/router/match-api.ts +1 -0
  66. package/src/router/match-context.ts +3 -0
  67. package/src/router/match-middleware/background-revalidation.ts +5 -4
  68. package/src/router/match-middleware/cache-lookup.ts +5 -3
  69. package/src/router/navigation-snapshot.ts +10 -0
  70. package/src/router/segment-resolution/helpers.ts +49 -13
  71. package/src/router/segment-resolution/loader-cache.ts +4 -3
  72. package/src/router/segment-resolution/resolution-pass.ts +2 -0
  73. package/src/router/segment-resolution/revalidation.ts +57 -28
  74. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  75. package/src/router/types.ts +1 -1
  76. package/src/router.ts +7 -4
  77. package/src/rsc/request-stages.ts +4 -2
  78. package/src/rsc/shell-capture.ts +63 -58
  79. package/src/server/request-context.ts +49 -0
  80. package/src/testing/dispatch.ts +3 -0
  81. package/src/testing/render-route.tsx +204 -16
  82. package/src/testing/serve-shell-request.ts +19 -10
  83. package/src/types/error-types.ts +27 -8
  84. package/src/wire-params.ts +5 -0
@@ -26,24 +26,30 @@
26
26
  * - navigate() drives the navigation lifecycle as a click does:
27
27
  * useNavigation() reads loading from the call and idle with the commit,
28
28
  * and useLinkStatus().pending is true for a link to the destination until
29
- * the commit is handed to React. A `transition` spec plus a pending
29
+ * the commit, which React holds with it. A `transition` spec plus a pending
30
30
  * Promise seeded via navigate(url, { loaders }) commits through
31
31
  * production's commitInTransition (browser/partial-update.ts), which React
32
32
  * holds: useNavigation() stays loading and the held reader's
33
33
  * useLoader().isLoading reads true until the data lands. Without
34
34
  * `transition` the commit is urgent (production's same-structure hold is
35
- * not modeled). useAction().state stays idle: no action runs.
35
+ * not modeled). navigate(url, { streamUntil }) gives the navigation a
36
+ * response stream that stays open past the commit (isStreaming).
37
+ * useAction().state stays idle: no action runs.
36
38
  * - A spec's transition({ when }) decides every commit with production's
37
39
  * browser decision: navigate() and useRouter().push model kind "push",
38
- * useRouter().replace kind "replace", refresh() kind "revalidate".
39
- * Back/forward, action and optimistic clientUrls() swap decisions are
40
- * e2e territory.
40
+ * useRouter().replace kind "replace", refresh() kind "revalidate", and a
41
+ * back/forward that restores a page kind "pop". Action and optimistic
42
+ * clientUrls() swap decisions are e2e territory.
41
43
  * - Location state reaches readers the way it does in production:
42
44
  * NavigationProvider holds the committed entry's state next to the
43
45
  * payload, so a navigation whose loader the test holds keeps the entry
44
46
  * being left on screen with its own state. A `popstate` event is a
45
- * back/forward onto the entry history.state holds; the tree has one
46
- * location, so no page is restored with it. A server action's state, a
47
+ * back/forward onto the entry history.state holds. When it is the
48
+ * mounted entry or one a navigate() wrote, history still holds the state
49
+ * written there, and another page is on screen, its page is restored
50
+ * from the history cache, with the handle values it had, as production's
51
+ * restore is; otherwise the tree keeps its location. A stale entry's
52
+ * revalidation, a restore that fetches, a server action's state, a
47
53
  * redirect's and an intercept's are e2e territory.
48
54
  * What it DOES cover: client hooks that read NavigationProvider /
49
55
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
@@ -361,8 +367,37 @@ export interface TestRouterHandle {
361
367
  * `handles`). They arrive on the response's handle stream, so `useHandle`
362
368
  * readers take them with the page's commit: while React holds the page,
363
369
  * readers keep the values of the page being left.
370
+ *
371
+ * `options.streamUntil` gives the navigation a response stream, as a real
372
+ * response has: it opens in a task after the call, before the commit (the
373
+ * response arrives, and `useNavigation().pendingUrl` clears), and stays open
374
+ * until the promise settles (a page that streams a tail past its commit:
375
+ * `isStreaming` reads true until then). Without it the navigation has no
376
+ * stream. A navigation that a newer one or a back/forward aborts before its
377
+ * response arrives opens none and commits nothing; one aborted after its
378
+ * response arrived stops counting as streaming at the abort, as in
379
+ * production, where its response streams on and is ignored.
380
+ *
381
+ * @example
382
+ * const tail = deferred();
383
+ * await router.navigate("/products/2", { streamUntil: tail.promise });
384
+ * // useNavigation(): idle, isStreaming true
385
+ * await act(async () => tail.resolve());
386
+ * // useNavigation(): idle, isStreaming false
387
+ *
388
+ * A navigation started while React holds a page shows its page once the
389
+ * call resolves, with or without act(). A `<Link>` click over a held page
390
+ * still goes inside `await act(async () => ...)` (docs/testing.md).
391
+ *
392
+ * @example
393
+ * await router.navigate("/products/2", { loaders: [[ProductLoader, next]] });
394
+ * // React holds /products/2
395
+ * await router.navigate("/products/3", { transition: false });
396
+ * // /products/3 is on screen
364
397
  */
365
- navigate(url: string, options?: Pick<RenderRouteOptions, "loaders" | "handles"> & Pick<NavigateOptions, "state" | "replace" | "transition">): Promise<void>;
398
+ navigate(url: string, options?: Pick<RenderRouteOptions, "loaders" | "handles"> & Pick<NavigateOptions, "state" | "replace" | "transition"> & {
399
+ streamUntil?: Promise<unknown>;
400
+ }): Promise<void>;
366
401
  /**
367
402
  * Re-render the current location, as router.refresh() does: a spec's
368
403
  * transition({ when }) decides it with kind "revalidate" (`to` is `from`).
@@ -81,7 +81,13 @@ export interface ServeShellRequestOptions<TEnv = any> {
81
81
  * and segments take an action's revalidation defaults; it renders live, as
82
82
  * an action does, so neither the route cache nor the document cache answers
83
83
  * or stores it, and a `ppr` route's `replayStatus` is
84
- * `{ outcome: "BYPASS", reason: "stale" }`.
84
+ * `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error
85
+ * segment of a thrown action's boundary the page at `from` shows
86
+ * (`_rsc_boundary`), as the follow-up an earlier action owes that page
87
+ * sends it, with `stale: true` and no `segments`: the response renders
88
+ * every segment above the boundary and nothing at or below it, claims the
89
+ * boundary in `matched` and sends no segment for it. The server reads it
90
+ * only beside `stale`.
85
91
  */
86
92
  partial?: true | {
87
93
  from?: string;
@@ -91,6 +97,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
91
97
  fill?: boolean;
92
98
  interceptSource?: string;
93
99
  stale?: boolean;
100
+ boundary?: string;
94
101
  };
95
102
  /**
96
103
  * POST this form as a submit before hydration: the no-JS request the
@@ -67,19 +67,38 @@ export interface OnErrorContext<TEnv = any> {
67
67
  actionId?: string;
68
68
  /** Environment/bindings (platform context) */
69
69
  env?: TEnv;
70
- /** Duration from request start to error (milliseconds) */
70
+ /**
71
+ * Duration from request start to error (milliseconds). For a `background`
72
+ * error it counts from the start of the request that started the work, so
73
+ * it includes that response and any wait before the work ran.
74
+ */
71
75
  duration?: number;
72
76
  /**
73
77
  * Whether this is a partial/navigation request, on an error the segment
74
- * walk catches (a handler that throws, a DSL loader that rejects): `true`
75
- * for a navigation, unset for a document. An error in the background
76
- * re-render of a `cache()` record (a stale HIT's refresh, the write of a
77
- * navigation that kept a layout mounted) reports the request that started
78
- * it. A streamed (`loading()`) handler's rejection does not carry it, in
79
- * either render; the `routing` phase reports `false`.
78
+ * walk catches (a handler that throws, a streamed `loading()` handler that
79
+ * rejects, a DSL loader that rejects): `true` for a navigation, unset for a
80
+ * document. A `cache()` record's refresh or proactive write reports the
81
+ * request that started it; a PPR shell capture renders a document. The
82
+ * `routing` phase reports `false`.
80
83
  */
81
84
  isPartial?: boolean;
82
- /** Whether an error boundary caught the error */
85
+ /**
86
+ * `true` when the error came from work whose output goes to a cache and
87
+ * never to the response of the request that started it: a stale `cache()`
88
+ * record's refresh, the write of a navigation that kept a layout mounted, a
89
+ * stale document cache entry's re-run, a PPR shell capture, or a stale
90
+ * `"use cache"` function's or cached loader's refresh, the work's own
91
+ * failure included. `request` and `url` are the starting request's (a
92
+ * shell capture's handler errors name the page it captures). A
93
+ * `router.prerender()` warm's own render is its request's: unset, while a
94
+ * shell capture it makes is `true`. Unset for every other error.
95
+ */
96
+ background?: boolean;
97
+ /**
98
+ * Whether an error boundary caught the error and its fallback reached the
99
+ * response. `false` on a `background` error: the failed render is never
100
+ * stored, so no visitor sees the fallback.
101
+ */
83
102
  handledByBoundary?: boolean;
84
103
  /** Stack trace (if available) */
85
104
  stack?: string;
@@ -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);
@@ -4485,13 +4491,13 @@ import {
4485
4491
  Component as Component3,
4486
4492
  createElement as createElement4,
4487
4493
  isValidElement as isValidElement2,
4488
- useContext as useContext17,
4494
+ useContext as useContext18,
4489
4495
  useMemo as useMemo9,
4490
4496
  Suspense as Suspense2
4491
4497
  } from "react";
4492
4498
 
4493
4499
  // src/browser/react/use-handle.ts
4494
- import { useContext, useState, useEffect, useRef } from "react";
4500
+ import { useContext as useContext2, useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
4495
4501
 
4496
4502
  // src/missing-id-error.ts
4497
4503
  var SELF_FILES = /* @__PURE__ */ new Set([
@@ -4569,6 +4575,19 @@ var NavigationStoreContext = createContext(null);
4569
4575
  var LocationStateContext = createContext(void 0);
4570
4576
  var HydrationBarrierContext = createContext(0);
4571
4577
 
4578
+ // src/browser/react/use-route-state.ts
4579
+ import {
4580
+ useContext,
4581
+ useEffect,
4582
+ useLayoutEffect,
4583
+ useRef,
4584
+ useState
4585
+ } from "react";
4586
+ var NO_ROUTE = {
4587
+ location: new URL("/", "http://localhost"),
4588
+ params: Object.freeze({})
4589
+ };
4590
+
4572
4591
  // src/handles/meta.ts
4573
4592
  function isUnsetDescriptor(descriptor) {
4574
4593
  return typeof descriptor === "object" && descriptor !== null && "unset" in descriptor && typeof descriptor.unset === "string";
@@ -4698,18 +4717,18 @@ function collectMeta(segments) {
4698
4717
  var Meta = createHandle(collectMeta, "__rsc_router_meta__");
4699
4718
 
4700
4719
  // src/theme/theme-context.ts
4701
- import { createContext as createContext2, useContext as useContext2 } from "react";
4720
+ import { createContext as createContext2, useContext as useContext3 } from "react";
4702
4721
  var ThemeContext = createContext2(null);
4703
4722
 
4704
4723
  // src/browser/react/nonce-context.ts
4705
- import { createContext as createContext3, useContext as useContext3 } from "react";
4724
+ import { createContext as createContext3, useContext as useContext4 } from "react";
4706
4725
  var NonceContext = createContext3(void 0);
4707
4726
 
4708
4727
  // src/handles/MetaTags.tsx
4709
4728
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4710
4729
 
4711
4730
  // src/handles/Scripts.tsx
4712
- import { useState as useState2 } from "react";
4731
+ import { useState as useState3 } from "react";
4713
4732
 
4714
4733
  // src/handles/script.ts
4715
4734
  function validateConfigDev(config) {
@@ -4763,7 +4782,7 @@ var Script = createHandle(collectScripts, "__rsc_router_script__");
4763
4782
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4764
4783
 
4765
4784
  // src/browser/react/ScrollRestoration.tsx
4766
- import { useEffect as useEffect2 } from "react";
4785
+ import { useEffect as useEffect3 } from "react";
4767
4786
 
4768
4787
  // src/browser/scroll-restoration.ts
4769
4788
  init_logging();
@@ -4777,12 +4796,12 @@ import { Component, Suspense, use as use2, useMemo as useMemo3 } from "react";
4777
4796
 
4778
4797
  // src/outlet-provider.tsx
4779
4798
  import {
4780
- useContext as useContext4,
4799
+ useContext as useContext5,
4781
4800
  useMemo
4782
4801
  } from "react";
4783
4802
 
4784
4803
  // src/suspense-audit-react.tsx
4785
- import { useEffect as useEffect3, useLayoutEffect, useRef as useRef2 } from "react";
4804
+ import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
4786
4805
 
4787
4806
  // src/suspense-audit.ts
4788
4807
  init_internal_debug();
@@ -4797,13 +4816,13 @@ import { jsx as jsx3 } from "react/jsx-runtime";
4797
4816
  import { createElement as createElement2 } from "react";
4798
4817
 
4799
4818
  // src/loader-redirect.tsx
4800
- import { useEffect as useEffect4 } from "react";
4819
+ import { useEffect as useEffect5 } from "react";
4801
4820
 
4802
4821
  // src/browser/react/use-router.ts
4803
- import { useContext as useContext6, useMemo as useMemo2 } from "react";
4822
+ import { useContext as useContext7, useMemo as useMemo2 } from "react";
4804
4823
 
4805
4824
  // src/browser/react/use-mount.ts
4806
- import { useContext as useContext5 } from "react";
4825
+ import { useContext as useContext6 } from "react";
4807
4826
 
4808
4827
  // src/browser/react/mount-context.ts
4809
4828
  import {
@@ -4996,12 +5015,12 @@ import {
4996
5015
  startTransition,
4997
5016
  use as use3,
4998
5017
  useCallback,
4999
- useContext as useContext7,
5000
- useEffect as useEffect5,
5018
+ useContext as useContext8,
5019
+ useEffect as useEffect6,
5001
5020
  useMemo as useMemo4,
5002
5021
  useOptimistic,
5003
- useRef as useRef3,
5004
- useState as useState3
5022
+ useRef as useRef4,
5023
+ useState as useState4
5005
5024
  } from "react";
5006
5025
 
5007
5026
  // src/loader-store.ts
@@ -5163,45 +5182,23 @@ function insertSegments(node, segments, index, leafBase, paramNames) {
5163
5182
  }
5164
5183
 
5165
5184
  // src/browser/react/use-navigation.ts
5166
- import {
5167
- useContext as useContext8,
5168
- useState as useState4,
5169
- useEffect as useEffect6,
5170
- useLayoutEffect as useLayoutEffect2,
5171
- startTransition as startTransition2,
5172
- useRef as useRef4
5173
- } from "react";
5185
+ import { useContext as useContext9, useEffect as useEffect7, useRef as useRef5 } from "react";
5174
5186
 
5175
5187
  // src/browser/react/use-pathname.ts
5176
- import { useContext as useContext9, useState as useState5, useEffect as useEffect7, useRef as useRef5 } from "react";
5188
+ import { useContext as useContext10 } from "react";
5177
5189
 
5178
5190
  // src/client-urls/optimistic-location.ts
5179
5191
  import { createContext as createContext6 } from "react";
5180
5192
  var OptimisticLocationContext = createContext6(null);
5181
5193
 
5182
5194
  // src/browser/react/use-search-params.ts
5183
- import {
5184
- useCallback as useCallback2,
5185
- useContext as useContext10,
5186
- useEffect as useEffect8,
5187
- useMemo as useMemo5,
5188
- useRef as useRef6,
5189
- useState as useState6
5190
- } from "react";
5195
+ import { useCallback as useCallback2, useContext as useContext11, useMemo as useMemo5 } from "react";
5191
5196
 
5192
5197
  // src/browser/react/use-params.ts
5193
- import { useContext as useContext11, useState as useState7, useEffect as useEffect9, useRef as useRef7 } from "react";
5194
- var EMPTY_PARAMS = Object.freeze({});
5198
+ import { useContext as useContext12 } from "react";
5195
5199
 
5196
5200
  // 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";
5201
+ import { useContext as useContext13, useState as useState5, useEffect as useEffect8, useRef as useRef6 } from "react";
5205
5202
 
5206
5203
  // src/browser/event-controller.ts
5207
5204
  init_notify_listeners();
@@ -5264,31 +5261,31 @@ function subscribeToLocationChange(eventController, listener) {
5264
5261
  }
5265
5262
 
5266
5263
  // src/browser/react/use-segments.ts
5267
- import { useContext as useContext13, useEffect as useEffect11, useRef as useRef9 } from "react";
5264
+ import { useContext as useContext14, useEffect as useEffect9, useRef as useRef7 } from "react";
5268
5265
 
5269
5266
  // src/browser/react/NavigationProvider.tsx
5270
5267
  import {
5271
- useState as useState11,
5272
- useEffect as useEffect13,
5268
+ useState as useState8,
5269
+ useEffect as useEffect11,
5273
5270
  useLayoutEffect as useLayoutEffect4,
5274
5271
  useCallback as useCallback4,
5275
5272
  useMemo as useMemo7,
5276
- useRef as useRef11,
5273
+ useRef as useRef9,
5277
5274
  use as use4,
5278
- startTransition as startTransition4
5275
+ startTransition as startTransition2
5279
5276
  } from "react";
5280
5277
 
5281
5278
  // src/root-error-boundary.tsx
5282
- import { Component as Component2, useState as useState9 } from "react";
5279
+ import { Component as Component2, useState as useState6 } from "react";
5283
5280
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
5284
5281
 
5285
5282
  // src/theme/ThemeProvider.tsx
5286
5283
  import {
5287
5284
  useCallback as useCallback3,
5288
- useEffect as useEffect12,
5285
+ useEffect as useEffect10,
5289
5286
  useLayoutEffect as useLayoutEffect3,
5290
5287
  useMemo as useMemo6,
5291
- useState as useState10
5288
+ useState as useState7
5292
5289
  } from "react";
5293
5290
 
5294
5291
  // src/theme/constants.ts
@@ -5322,21 +5319,18 @@ import { jsx as jsx7 } from "react/jsx-runtime";
5322
5319
  import {
5323
5320
  forwardRef,
5324
5321
  useCallback as useCallback5,
5325
- useContext as useContext15,
5326
- useEffect as useEffect15,
5322
+ useContext as useContext16,
5323
+ useEffect as useEffect13,
5327
5324
  useMemo as useMemo8,
5328
- useRef as useRef13
5325
+ useRef as useRef10
5329
5326
  } from "react";
5330
5327
 
5331
5328
  // src/browser/react/use-link-status.ts
5332
5329
  import {
5333
5330
  createContext as createContext7,
5334
- useContext as useContext14,
5335
- useState as useState12,
5336
- useEffect as useEffect14,
5337
- useRef as useRef12,
5338
- useOptimistic as useOptimistic3,
5339
- startTransition as startTransition5
5331
+ useContext as useContext15,
5332
+ useState as useState9,
5333
+ useEffect as useEffect12
5340
5334
  } from "react";
5341
5335
  var LinkContext = createContext7(
5342
5336
  null
@@ -5537,14 +5531,14 @@ var Link = forwardRef(function Link2({
5537
5531
  onClick,
5538
5532
  ...props
5539
5533
  }, ref) {
5540
- const ctx = useContext15(NavigationStoreContext);
5534
+ const ctx = useContext16(NavigationStoreContext);
5541
5535
  const isExternal = isExternalUrl(to, ctx?.origin);
5542
5536
  const resolvedTo = useMemo8(
5543
5537
  () => isExternal ? to : prefixBasename(to, ctx?.basename),
5544
5538
  [to, isExternal, ctx?.basename]
5545
5539
  );
5546
5540
  const configuredStrategy = prefetch ?? ctx?.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
5547
- const internalRef = useRef13(null);
5541
+ const internalRef = useRef10(null);
5548
5542
  const setRef = useCallback5(
5549
5543
  (node) => {
5550
5544
  internalRef.current = node;
@@ -5556,7 +5550,7 @@ var Link = forwardRef(function Link2({
5556
5550
  },
5557
5551
  [ref]
5558
5552
  );
5559
- const stateRef = useRef13(state);
5553
+ const stateRef = useRef10(state);
5560
5554
  stateRef.current = state;
5561
5555
  const handleClick = useCallback5(
5562
5556
  (e) => {
@@ -5613,7 +5607,7 @@ var Link = forwardRef(function Link2({
5613
5607
  );
5614
5608
  }
5615
5609
  }, [configuredStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
5616
- useEffect15(() => {
5610
+ useEffect13(() => {
5617
5611
  if (isExternal || !ctx?.store) return;
5618
5612
  if (configuredStrategy !== "viewport" && configuredStrategy !== "render" && configuredStrategy !== "adaptive") {
5619
5613
  return;
@@ -5733,7 +5727,7 @@ var Breadcrumbs = createHandle(
5733
5727
  );
5734
5728
 
5735
5729
  // src/browser/react/location-state.ts
5736
- import { useContext as useContext16, useEffect as useEffect16 } from "react";
5730
+ import { useContext as useContext17, useEffect as useEffect14 } from "react";
5737
5731
 
5738
5732
  // src/browser/react/use-href.tsx
5739
5733
  import { useCallback as useCallback6 } from "react";
@@ -5936,6 +5930,7 @@ var DERIVED_RENDER_BARRIER_DESCRIPTOR = {
5936
5930
  ...RENDER_BARRIER_DESCRIPTOR,
5937
5931
  enumerable: false
5938
5932
  };
5933
+ var backgroundWork = new AsyncLocalStorage6();
5939
5934
 
5940
5935
  // src/route-definition/redirect.ts
5941
5936
  init_redirect_origin();
@@ -5948,11 +5943,11 @@ import {
5948
5943
  useDeferredValue,
5949
5944
  useLayoutEffect as useLayoutEffect5,
5950
5945
  useMemo as useMemo10,
5951
- useState as useState13
5946
+ useState as useState10
5952
5947
  } from "react";
5953
5948
 
5954
5949
  // src/client-urls/navigation.ts
5955
- import { startTransition as startTransition6 } from "react";
5950
+ import { startTransition as startTransition3 } from "react";
5956
5951
 
5957
5952
  // src/client-urls/client-root.tsx
5958
5953
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -7146,7 +7141,7 @@ function resolveVersionsFrom(table, routerId2) {
7146
7141
  // package.json
7147
7142
  var package_default = {
7148
7143
  name: "@rangojs/router",
7149
- version: "0.28.0",
7144
+ version: "0.30.0",
7150
7145
  description: "Django-inspired RSC router with composable URL patterns",
7151
7146
  keywords: [
7152
7147
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -450,16 +450,12 @@ function StockPanel() {
450
450
  }
451
451
 
452
452
  function SaveStock({ sku }: { sku: string }) {
453
+ // Kept until the next save starts.
453
454
  const { revalidationError } = useAction(saveStock);
454
- const [stale, setStale] = useState(false);
455
- // useAction resets to idle once the action has settled: keep the notice.
456
- useEffect(() => {
457
- if (revalidationError) setStale(true);
458
- }, [revalidationError]);
459
455
  return (
460
456
  <form action={saveStock.bind(null, sku)}>
461
457
  <button>Save</button>
462
- {stale && (
458
+ {revalidationError != null && (
463
459
  <p role="status">Saved. The stock count could not be refreshed.</p>
464
460
  )}
465
461
  </form>
@@ -734,8 +730,10 @@ a hard load of the target URL renders the full route.
734
730
  values during SSR (live request seeds the SSR store; on ppr routes the
735
731
  capture/resume renders seed the shell key's own sorted search — search is
736
732
  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()`)
733
+ `useNavigation`/`useLinkStatus` report the canonical nav (while the frame
734
+ shows, `useNavigation()` is `state: "loading"` with `location` the
735
+ committed URL, even though the URL hooks already report the frame's
736
+ destination), but a reader inside optimistically-swapped content (destination WITH `loading()`)
739
737
  unmounts at click — put status readers in surviving chrome. Errors: wrap
740
738
  throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
741
739
  (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,41 @@ 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
+ - Once the call settles, `state` is `idle` and the rest of its state
200
+ (`result`, `error`, `revalidationError`, `payload`, `actionId`) stays until
201
+ the same action is called again: through navigations and Back/Forward, and
202
+ in a component that mounts after the call settled. Every reader of the
203
+ same action (a function or a full id) reads the same call; a string reader
204
+ follows the latest call of any action with that export name. The next
205
+ call reads `loading` with none of them, then its own. A document load
206
+ starts with nothing.
207
+ - When the latest call returns before an earlier one, the hook shows its
208
+ result. The earlier call settling afterwards changes nothing the hook
209
+ reports, a result or an error alike. That call still runs to its end: its
210
+ promise resolves or rejects for its caller, its revalidation re-renders the
211
+ page, and an error-boundary response replaces it.
212
+ - A latest call that was aborted reports nothing: `idle`, with no result or
213
+ error. The hook does not fall back to an earlier call. The router aborts
214
+ the other calls in flight when a call's response is an error boundary.
215
+ - `useNavigation()` stays `loading` while any call runs.
216
+
217
+ Which calls count as the same action depends on what you pass:
218
+
219
+ - A function, or a full id (`"src/actions/cart.ts#addToCart"`): that action
220
+ only.
221
+ - A string name (`"addToCart"`): every action with that export name, in any
222
+ `"use server"` file. The hook follows the latest call among all of them.
223
+
224
+ The hook tracks one call at a time, not one per argument. Every
225
+ `AddToCartButton` below reads the same `useAction(addToCart)`, so each shows
226
+ the latest call from any button, whatever its `productId`. A reader that
227
+ filters on `payload` (`payload?.[0] === productId`) cannot track an earlier
228
+ call once a later one starts: from then on the hook reports the later call
229
+ only. For per-call state, keep it with the call: `useActionState` or
230
+ `useTransition` in the component that makes it.
197
231
 
198
232
  ```tsx
199
233
  "use client";
@@ -231,6 +265,10 @@ const isLoading = useAction(addToCart, (s) => s.state === "loading");
231
265
  const isAdding = useAction("addToCart", (s) => s.state === "loading");
232
266
  ```
233
267
 
268
+ Each state change commits urgently, so it shows at once, also while React
269
+ holds a navigation's page. It starts no transition of its own, so it never
270
+ takes that page's transition types.
271
+
234
272
  Pass an action imported directly into the client component, or its export
235
273
  name as a string. An action received as a prop from a server component has
236
274
  lost the metadata `useAction` needs and throws; use the string form there.