@rangojs/router 0.24.4 → 0.25.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/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
useContext,
|
|
5
|
-
useState,
|
|
6
|
-
useEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
useOptimistic,
|
|
9
|
-
startTransition,
|
|
10
|
-
} from "react";
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
11
4
|
import type { Handle } from "../../handle.js";
|
|
12
5
|
import { collectHandleData } from "../../handle.js";
|
|
13
6
|
import type { HandleData } from "../types.js";
|
|
14
7
|
import { NavigationStoreContext } from "./context.js";
|
|
15
8
|
import { shallowEqual } from "./shallow-equal.js";
|
|
9
|
+
import type { RouteStateSignal } from "../event-controller.js";
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
12
|
* Hook to access collected handle data.
|
|
@@ -43,77 +37,75 @@ export function useHandle<T, A, S>(
|
|
|
43
37
|
): Rango.FlightSerialize<A> | S {
|
|
44
38
|
const ctx = useContext(NavigationStoreContext);
|
|
45
39
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const collected = collectHandleData(
|
|
49
|
-
handle,
|
|
50
|
-
{},
|
|
51
|
-
[],
|
|
52
|
-
) as Rango.FlightSerialize<A>;
|
|
53
|
-
return selector ? selector(collected) : collected;
|
|
54
|
-
}
|
|
40
|
+
const selectorRef = useRef(selector);
|
|
41
|
+
selectorRef.current = selector;
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
// Until every boundary has hydrated the store returns the state the
|
|
44
|
+
// document's HTML was rendered with (EventController.lockHydration), so a
|
|
45
|
+
// reader hydrating late reads what SSR rendered (issue #1035).
|
|
46
|
+
const collect = (): Rango.FlightSerialize<A> | S => {
|
|
47
|
+
const state = ctx?.eventController.getHandleState();
|
|
60
48
|
const collected = collectHandleData(
|
|
61
49
|
handle,
|
|
62
|
-
state
|
|
63
|
-
state
|
|
50
|
+
state?.data ?? {},
|
|
51
|
+
state?.segmentOrder ?? [],
|
|
64
52
|
) as Rango.FlightSerialize<A>;
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
68
|
-
|
|
69
|
-
const prevValueRef = useRef(value);
|
|
70
|
-
prevValueRef.current = value;
|
|
53
|
+
return selectorRef.current ? selectorRef.current(collected) : collected;
|
|
54
|
+
};
|
|
71
55
|
|
|
72
|
-
const
|
|
73
|
-
selectorRef.current = selector;
|
|
56
|
+
const [value, setValue] = useCommitAnchoredState(collect);
|
|
74
57
|
|
|
75
58
|
useEffect(() => {
|
|
76
59
|
if (!ctx) return;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
handle,
|
|
83
|
-
currentHandleState.data,
|
|
84
|
-
currentHandleState.segmentOrder,
|
|
85
|
-
) as Rango.FlightSerialize<A>;
|
|
86
|
-
const currentValue = selectorRef.current
|
|
87
|
-
? selectorRef.current(currentCollected)
|
|
88
|
-
: currentCollected;
|
|
89
|
-
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
90
|
-
prevValueRef.current = currentValue;
|
|
91
|
-
setValue(currentValue);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return ctx.eventController.subscribeToHandles(() => {
|
|
95
|
-
const state = ctx.eventController.getHandleState();
|
|
96
|
-
const isAction =
|
|
97
|
-
ctx.eventController.getState().inflightActions.length > 0;
|
|
98
|
-
const collected = collectHandleData(
|
|
99
|
-
handle,
|
|
100
|
-
state.data,
|
|
101
|
-
state.segmentOrder,
|
|
102
|
-
) as Rango.FlightSerialize<A>;
|
|
103
|
-
const nextValue = selectorRef.current
|
|
104
|
-
? selectorRef.current(collected)
|
|
105
|
-
: collected;
|
|
106
|
-
|
|
107
|
-
if (!shallowEqual(nextValue, prevValueRef.current)) {
|
|
108
|
-
prevValueRef.current = nextValue;
|
|
109
|
-
startTransition(() => {
|
|
110
|
-
// Skip optimistic update during actions to prevent Suspense fallback
|
|
111
|
-
if (!isAction) setOptimisticValue(nextValue);
|
|
112
|
-
setValue(nextValue);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
});
|
|
60
|
+
const update = (signal?: RouteStateSignal): void =>
|
|
61
|
+
setValue(collect(), signal);
|
|
62
|
+
// A (possibly new) handle on an idle page reads the current state.
|
|
63
|
+
update();
|
|
64
|
+
return ctx.eventController.subscribeToHandles(update);
|
|
116
65
|
}, [handle]);
|
|
117
66
|
|
|
118
|
-
return
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Route state the controller's handle notifications drive (useHandle,
|
|
72
|
+
* useSegments) commits with the page it belongs to. NavigationProvider
|
|
73
|
+
* flushes the controller inside every payload update, so a value known then
|
|
74
|
+
* is set in the page's lane. A page handed over urgently (signal "release")
|
|
75
|
+
* takes a value that arrives before it commits the same way: set at default
|
|
76
|
+
* priority, it renders in the page's lane, and the page's effects read it.
|
|
77
|
+
* A page in a transition (signal "anchor") does not: until it commits
|
|
78
|
+
* (releaseRouteState() from the provider's layout effect, signal "release"),
|
|
79
|
+
* a new value waits, and is applied in the commit's task, right after the
|
|
80
|
+
* page's own commit. In the page's lane, a value that suspends in a revealed
|
|
81
|
+
* <Suspense> (a crumb's content) would hold the page; in a lane of its own,
|
|
82
|
+
* it commits over the page being left and takes the page's transition types
|
|
83
|
+
* (#1165); and a transition entangled with the page misses its commit when it
|
|
84
|
+
* is set after React rendered the page, before its view transition's
|
|
85
|
+
* mutation, and starts a view transition of its own. A value shallow-equal to
|
|
86
|
+
* the last one sets nothing, so a reader renders only when its value moves.
|
|
87
|
+
*/
|
|
88
|
+
export function useCommitAnchoredState<T>(
|
|
89
|
+
init: () => T,
|
|
90
|
+
): [T, (value: T, signal?: RouteStateSignal) => void] {
|
|
91
|
+
// An app that enables the React Compiler compiles the router too; its memo
|
|
92
|
+
// cache gains nothing here and cost 107 B gzip in the router chunk.
|
|
93
|
+
"use no memo";
|
|
94
|
+
const [value, setValue] = useState(init);
|
|
95
|
+
const ref = useRef<{ last: T; held?: boolean; waiting?: boolean }>({
|
|
96
|
+
last: value,
|
|
97
|
+
});
|
|
98
|
+
const [set] = useState(() => (next: T, signal?: RouteStateSignal): void => {
|
|
99
|
+
const r = ref.current;
|
|
100
|
+
if (signal) r.waiting = signal === "anchor";
|
|
101
|
+
if (!shallowEqual(next, r.last)) {
|
|
102
|
+
r.last = next;
|
|
103
|
+
r.held = true;
|
|
104
|
+
}
|
|
105
|
+
// The anchor's own change goes in the page's lane, with the page.
|
|
106
|
+
if (!r.held || (r.waiting && signal !== "anchor")) return;
|
|
107
|
+
r.held = false;
|
|
108
|
+
setValue(() => r.last);
|
|
109
|
+
});
|
|
110
|
+
return [value, set];
|
|
119
111
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback } from "react";
|
|
4
4
|
import { href, type ValidPaths } from "../../href-client.js";
|
|
5
|
+
import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
|
|
5
6
|
import { useMount } from "./use-mount.js";
|
|
7
|
+
import { useParams } from "./use-params.js";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Client-side hook for mount-aware URL resolution.
|
|
@@ -11,6 +13,12 @@ import { useMount } from "./use-mount.js";
|
|
|
11
13
|
* include() mount prefix. Inside an include("/shop", ...) scope,
|
|
12
14
|
* the returned function resolves local paths relative to /shop.
|
|
13
15
|
*
|
|
16
|
+
* Under a parameterized mount (`include("/:accountId", ...)`) the mount's
|
|
17
|
+
* params are filled from the current route params, as `useReverse` does:
|
|
18
|
+
* `href("/issues")` at `/acme/board` returns `/acme/issues`. A mount param
|
|
19
|
+
* missing from the current match throws `Missing param` when the returned
|
|
20
|
+
* function is called.
|
|
21
|
+
*
|
|
14
22
|
* For absolute paths (outside the current mount), use the bare
|
|
15
23
|
* href() function directly instead.
|
|
16
24
|
*
|
|
@@ -37,11 +45,22 @@ import { useMount } from "./use-mount.js";
|
|
|
37
45
|
*/
|
|
38
46
|
export function useHref(): (path: `/${string}`) => string {
|
|
39
47
|
const mount = useMount();
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
48
|
+
// The filled mount (it starts with "/"), or the Missing param message. The
|
|
49
|
+
// selector also runs in the params subscription, so it must not throw, and
|
|
50
|
+
// a string lets useParams skip the re-render, and keeps the returned
|
|
51
|
+
// function stable, unless the mount's own values change.
|
|
52
|
+
const base = useParams((p) => {
|
|
53
|
+
try {
|
|
54
|
+
return substitutePatternParams(mount, p, mount);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return (e as Error).message;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
43
59
|
return useCallback(
|
|
44
|
-
(path: `/${string}`) =>
|
|
45
|
-
|
|
60
|
+
(path: `/${string}`) => {
|
|
61
|
+
if (!base.startsWith("/")) throw new Error(base);
|
|
62
|
+
return href(path as ValidPaths, base);
|
|
63
|
+
},
|
|
64
|
+
[base],
|
|
46
65
|
);
|
|
47
66
|
}
|
|
@@ -100,7 +100,7 @@ export function useLinkStatus(): LinkStatus {
|
|
|
100
100
|
// to a non-idle (loading) state. Used to decide whether to emit a release
|
|
101
101
|
// update when returning to idle, so the optimistic store doesn't stay pinned
|
|
102
102
|
// to `true` if a parent transition (e.g. the <Link> click / view transition
|
|
103
|
-
// commit) is still pending.
|
|
103
|
+
// commit) is still pending.
|
|
104
104
|
const optimisticPinnedRef = useRef(false);
|
|
105
105
|
|
|
106
106
|
const [pending, setOptimisticPending] = useOptimistic(basePending);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useContext,
|
|
5
5
|
useState,
|
|
6
6
|
useEffect,
|
|
7
|
-
|
|
7
|
+
useLayoutEffect,
|
|
8
8
|
startTransition,
|
|
9
9
|
useRef,
|
|
10
10
|
} from "react";
|
|
@@ -46,22 +46,19 @@ export function useNavigation<T>(
|
|
|
46
46
|
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const [
|
|
49
|
+
const [value, setValue] = useState<T | PublicNavigationState>(() => {
|
|
50
50
|
const publicState = toPublicState(
|
|
51
51
|
ctx.eventController.getHydrationSnapshot()?.state ??
|
|
52
52
|
ctx.eventController.getState(),
|
|
53
53
|
);
|
|
54
54
|
return selector ? selector(publicState) : publicState;
|
|
55
55
|
});
|
|
56
|
-
const prevState = useRef(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const optimisticPinnedRef = useRef(false);
|
|
63
|
-
|
|
64
|
-
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
56
|
+
const prevState = useRef(value);
|
|
57
|
+
// The value on screen: a set value that differs has not committed yet.
|
|
58
|
+
const committedRef = useRef(value);
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
committedRef.current = value;
|
|
61
|
+
});
|
|
65
62
|
|
|
66
63
|
// Store selector in a ref so the subscription callback always uses the
|
|
67
64
|
// latest selector without re-subscribing on every render (inline functions
|
|
@@ -71,10 +68,28 @@ export function useNavigation<T>(
|
|
|
71
68
|
const selectorRef = useRef(selector);
|
|
72
69
|
selectorRef.current = selector;
|
|
73
70
|
|
|
74
|
-
// Subscribe to event controller state changes (only runs on client)
|
|
71
|
+
// Subscribe to event controller state changes (only runs on client).
|
|
72
|
+
//
|
|
73
|
+
// The value commits in the lane of the notification that carries it, as
|
|
74
|
+
// the location hooks' values do: NavigationProvider flushes a navigation's
|
|
75
|
+
// notification into the transition that commits its tree, so idle commits
|
|
76
|
+
// with the destination's location (useSearchParams, usePathname,
|
|
77
|
+
// useParams). An optimistic update renders in the sync lane, ahead of that
|
|
78
|
+
// transition, and put idle on screen with the location being left (#1164).
|
|
75
79
|
useEffect(() => {
|
|
80
|
+
let last: DerivedNavigationState | undefined;
|
|
76
81
|
const update = () => {
|
|
77
82
|
const currentState = ctx.eventController.getState();
|
|
83
|
+
// Neither the state nor the location moved (the stream starting or
|
|
84
|
+
// ending): while a value of this hook's is held, a transition, which
|
|
85
|
+
// React entangles with it, so it never commits ahead of the location.
|
|
86
|
+
// Otherwise urgent: a transition of its own would start a view
|
|
87
|
+
// transition, and could commit ahead of a held page and take its
|
|
88
|
+
// transition types.
|
|
89
|
+
const quiet =
|
|
90
|
+
currentState.state === last?.state &&
|
|
91
|
+
currentState.location === last.location;
|
|
92
|
+
last = currentState;
|
|
78
93
|
const publicState = toPublicState(currentState);
|
|
79
94
|
const nextSelected = selectorRef.current
|
|
80
95
|
? selectorRef.current(publicState)
|
|
@@ -82,35 +97,10 @@ export function useNavigation<T>(
|
|
|
82
97
|
|
|
83
98
|
// Check if selected value has changed
|
|
84
99
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
100
|
+
const held = prevState.current !== committedRef.current;
|
|
85
101
|
prevState.current = nextSelected;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const hasInflightActions =
|
|
89
|
-
ctx.eventController.getInflightActions().size > 0;
|
|
90
|
-
|
|
91
|
-
const shouldPin = hasInflightActions || publicState.state !== "idle";
|
|
92
|
-
|
|
93
|
-
if (shouldPin) {
|
|
94
|
-
// Pin the optimistic store so the loading value shows immediately
|
|
95
|
-
// even if a parent transition (e.g. <Link> click) defers the
|
|
96
|
-
// urgent setBaseValue commit.
|
|
97
|
-
startTransition(() => {
|
|
98
|
-
setOptimisticValue(nextSelected);
|
|
99
|
-
});
|
|
100
|
-
optimisticPinnedRef.current = true;
|
|
101
|
-
} else if (optimisticPinnedRef.current) {
|
|
102
|
-
// Release a previously-pinned optimistic value. Without this,
|
|
103
|
-
// useOptimistic keeps returning the stale loading value while
|
|
104
|
-
// any parent transition is still pending, even after baseValue
|
|
105
|
-
// flipped to idle.
|
|
106
|
-
startTransition(() => {
|
|
107
|
-
setOptimisticValue(nextSelected);
|
|
108
|
-
});
|
|
109
|
-
optimisticPinnedRef.current = false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Always update base state so UI reflects current state
|
|
113
|
-
setBaseValue(nextSelected);
|
|
102
|
+
if (quiet && held) startTransition(() => setValue(nextSelected));
|
|
103
|
+
else setValue(nextSelected);
|
|
114
104
|
}
|
|
115
105
|
};
|
|
116
106
|
|
|
@@ -6,6 +6,8 @@ import { useMount } from "./use-mount.js";
|
|
|
6
6
|
import { prefetchDirect } from "../prefetch/loader.js";
|
|
7
7
|
import { getAppVersion } from "../app-version.js";
|
|
8
8
|
import { prefixBasename } from "../../router/basename.js";
|
|
9
|
+
import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
|
|
10
|
+
import { href, type ValidPaths } from "../../href-client.js";
|
|
9
11
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -22,7 +24,12 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
22
24
|
*
|
|
23
25
|
* RELATIVE paths (no leading `/`, no scheme/query/hash) resolve against the
|
|
24
26
|
* current include() mount: `router.push("cart")` inside `include("/shop")`
|
|
25
|
-
* navigates to `/shop/cart`.
|
|
27
|
+
* navigates to `/shop/cart`. Under a parameterized mount
|
|
28
|
+
* (`include("/:accountId", ...)`) the mount's params are filled from the
|
|
29
|
+
* current route params: `router.push("issues")` at `/acme/board` navigates
|
|
30
|
+
* to `/acme/issues`, and a missing mount param rejects the push/replace
|
|
31
|
+
* promise with `Missing param` (a prefetch skips the target).
|
|
32
|
+
* Absolute paths stay APP-absolute — unlike
|
|
26
33
|
* basename, the mount is scoped, and absolute pushes legitimately target
|
|
27
34
|
* outside it, so they are never auto-prefixed.
|
|
28
35
|
*
|
|
@@ -70,16 +77,32 @@ export function useRouter(): RouterInstance {
|
|
|
70
77
|
) {
|
|
71
78
|
return withBasename(url);
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
// The mount is the include()'s URL pattern: fill its params from the
|
|
81
|
+
// committed params at call time (not render time, so this object stays
|
|
82
|
+
// stable across navigations), as useHref does.
|
|
83
|
+
return withBasename(
|
|
84
|
+
href(
|
|
85
|
+
`/${relative}` as ValidPaths,
|
|
86
|
+
substitutePatternParams(
|
|
87
|
+
mount,
|
|
88
|
+
ctx!.eventController.getParams(),
|
|
89
|
+
mount,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
75
93
|
}
|
|
76
94
|
|
|
95
|
+
// push/replace are async so a target that cannot resolve (a missing
|
|
96
|
+
// mount param) rejects the returned promise instead of throwing.
|
|
77
97
|
return {
|
|
78
|
-
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
98
|
+
async push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
79
99
|
return ctx.navigate(resolveTarget(url), { ...options, replace: false });
|
|
80
100
|
},
|
|
81
101
|
|
|
82
|
-
replace(
|
|
102
|
+
async replace(
|
|
103
|
+
url: string,
|
|
104
|
+
options?: RouterNavigateOptions,
|
|
105
|
+
): Promise<void> {
|
|
83
106
|
return ctx.navigate(resolveTarget(url), { ...options, replace: true });
|
|
84
107
|
},
|
|
85
108
|
|
|
@@ -90,8 +113,16 @@ export function useRouter(): RouterInstance {
|
|
|
90
113
|
prefetch(url: string, options?: { key?: ":source" }): void {
|
|
91
114
|
const segmentState = ctx.store?.getSegmentState();
|
|
92
115
|
if (segmentState) {
|
|
116
|
+
let target: string;
|
|
117
|
+
try {
|
|
118
|
+
target = resolveTarget(url);
|
|
119
|
+
} catch {
|
|
120
|
+
// A prefetch is a hint: a target that cannot resolve (missing
|
|
121
|
+
// mount param) is skipped; push/replace report the error.
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
93
124
|
prefetchDirect(
|
|
94
|
-
|
|
125
|
+
target,
|
|
95
126
|
segmentState.currentSegmentIds,
|
|
96
127
|
getAppVersion(),
|
|
97
128
|
ctx.store?.getRouterId?.(),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useContext,
|
|
3
|
+
import { useContext, useEffect, useRef } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useCommitAnchoredState } from "./use-handle.js";
|
|
6
|
+
import type { RouteStateSignal } from "../event-controller.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Segments state returned by useSegments hook
|
|
@@ -71,7 +72,8 @@ export function useSegments<T>(
|
|
|
71
72
|
const selectorRef = useRef(selector);
|
|
72
73
|
selectorRef.current = selector;
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// Commits with the page it describes (useCommitAnchoredState).
|
|
76
|
+
const [state, setState] = useCommitAnchoredState<T | SegmentsState>(() => {
|
|
75
77
|
if (!ctx) {
|
|
76
78
|
const fallbackLocation = new URL("/", "http://localhost");
|
|
77
79
|
const fallbackState = buildSegmentsState(fallbackLocation, []);
|
|
@@ -88,8 +90,6 @@ export function useSegments<T>(
|
|
|
88
90
|
return selector ? selector(segmentsState) : segmentsState;
|
|
89
91
|
});
|
|
90
92
|
|
|
91
|
-
const prevState = useRef(state);
|
|
92
|
-
|
|
93
93
|
useEffect(() => {
|
|
94
94
|
if (!ctx) {
|
|
95
95
|
return;
|
|
@@ -103,7 +103,7 @@ export function useSegments<T>(
|
|
|
103
103
|
state: SegmentsState;
|
|
104
104
|
} | null = null;
|
|
105
105
|
|
|
106
|
-
const updateState = () => {
|
|
106
|
+
const updateState = (signal?: RouteStateSignal) => {
|
|
107
107
|
const location = ctx.eventController.getLocation() as URL;
|
|
108
108
|
const { routeSegmentIds } = ctx.eventController.getHandleState();
|
|
109
109
|
if (
|
|
@@ -118,17 +118,13 @@ export function useSegments<T>(
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
const sel = selectorRef.current;
|
|
121
|
-
|
|
122
|
-
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
123
|
-
prevState.current = nextSelected;
|
|
124
|
-
setState(nextSelected);
|
|
125
|
-
}
|
|
121
|
+
setState(sel ? sel(cache.state) : cache.state, signal);
|
|
126
122
|
};
|
|
127
123
|
|
|
128
124
|
// Catch up with a change between the seeding render and this effect.
|
|
129
125
|
updateState();
|
|
130
126
|
|
|
131
|
-
const unsubscribeNav = ctx.eventController.subscribe(updateState);
|
|
127
|
+
const unsubscribeNav = ctx.eventController.subscribe(() => updateState());
|
|
132
128
|
const unsubscribeHandles =
|
|
133
129
|
ctx.eventController.subscribeToHandles(updateState);
|
|
134
130
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
2
|
+
import { PREFETCH_FAILED_ROW } from "../prefetch-markers.js";
|
|
2
3
|
|
|
3
4
|
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
5
|
|
|
@@ -105,9 +106,11 @@ export function handleReloadHeader(
|
|
|
105
106
|
* end-of-stream, calling onComplete when done.
|
|
106
107
|
*
|
|
107
108
|
* `onComplete` receives `endedCleanly`: true only on a normal EOF drain, false
|
|
108
|
-
* on a read error
|
|
109
|
-
*
|
|
110
|
-
*
|
|
109
|
+
* on a read error, an abort (the signal cancelled the reader), or a body that
|
|
110
|
+
* ends with PREFETCH_FAILED_ROW (a prefetch whose render reported an error,
|
|
111
|
+
* rsc/prefetch-render-error.ts). Callers that gate a "fully complete" fast
|
|
112
|
+
* path (e.g. prefetch's `entry.complete`) must treat a non-clean end as
|
|
113
|
+
* incomplete — a broken stream is not complete data.
|
|
111
114
|
*
|
|
112
115
|
* If the response has no body, onComplete fires synchronously with `true`
|
|
113
116
|
* (an empty body is a clean, complete stream).
|
|
@@ -156,13 +159,16 @@ export function teeWithCompletion(
|
|
|
156
159
|
// loop breaks here normally, NOT in the .catch — re-check the signal to
|
|
157
160
|
// catch that case).
|
|
158
161
|
let cleanEof = false;
|
|
162
|
+
// The last five bytes read, across chunk boundaries.
|
|
163
|
+
let tail = "";
|
|
159
164
|
try {
|
|
160
165
|
while (true) {
|
|
161
|
-
const { done } = await reader.read();
|
|
166
|
+
const { done, value } = await reader.read();
|
|
162
167
|
if (done) {
|
|
163
|
-
cleanEof =
|
|
168
|
+
cleanEof = tail !== PREFETCH_FAILED_ROW;
|
|
164
169
|
break;
|
|
165
170
|
}
|
|
171
|
+
tail = (tail + String.fromCharCode(...value.slice(-5))).slice(-5);
|
|
166
172
|
}
|
|
167
173
|
} finally {
|
|
168
174
|
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
setPrefetchConcurrency,
|
|
38
38
|
setPrefetchDecoder,
|
|
39
39
|
} from "./prefetch/loader.js";
|
|
40
|
+
import { publishRangoMarker } from "./rango-marker.js";
|
|
40
41
|
import { setAppVersion } from "./app-version.js";
|
|
41
42
|
import {
|
|
42
43
|
isInterceptSegment,
|
|
@@ -185,6 +186,7 @@ export async function initBrowserApp(
|
|
|
185
186
|
} = options;
|
|
186
187
|
|
|
187
188
|
bootLog("initBrowserApp start");
|
|
189
|
+
publishRangoMarker();
|
|
188
190
|
// Before any location read (the history key, the navigation store, the
|
|
189
191
|
// event controller): the server rendered the marked request for the clean
|
|
190
192
|
// URL, so the client starts from the same one and hydration agrees.
|
|
@@ -248,7 +250,7 @@ export async function initBrowserApp(
|
|
|
248
250
|
|
|
249
251
|
// The document's entry: the transition({ when }) source of the first
|
|
250
252
|
// navigation, and of a later back/forward that leaves it.
|
|
251
|
-
store.rememberDisplayedEntry(initialPayload.metadata
|
|
253
|
+
store.rememberDisplayedEntry(initialPayload.metadata);
|
|
252
254
|
|
|
253
255
|
// Register the active store on the module-level handle and wire the
|
|
254
256
|
// jar-divergence observer before any getRangoState() read can detect a
|
|
@@ -75,7 +75,8 @@ let customGetKey: GetScrollKeyFunction | null = null;
|
|
|
75
75
|
/**
|
|
76
76
|
* Scroll key of the entry on screen. At popstate history.state already belongs
|
|
77
77
|
* to the destination, so this is the only way to save the page being left
|
|
78
|
-
* under its own key (see handleTraversalStart).
|
|
78
|
+
* under its own key (see handleTraversalStart). A traversal moves it when its
|
|
79
|
+
* restore commits (handleNavigationEnd), not at its event.
|
|
79
80
|
*/
|
|
80
81
|
let displayedKey: string | null = null;
|
|
81
82
|
|
|
@@ -283,10 +284,12 @@ export function cancelScrollRestorationPolling(): void {
|
|
|
283
284
|
*
|
|
284
285
|
* @param options.retryIfStreaming - If true, poll while streaming until we can scroll to target
|
|
285
286
|
* @param options.isStreaming - Function to check if streaming is in progress
|
|
287
|
+
* @param options.committed - React has committed the restored tree (a layout effect)
|
|
286
288
|
*/
|
|
287
289
|
export function restoreScrollPosition(options?: {
|
|
288
290
|
retryIfStreaming?: boolean;
|
|
289
291
|
isStreaming?: () => boolean;
|
|
292
|
+
committed?: boolean;
|
|
290
293
|
}): boolean {
|
|
291
294
|
// Clear any pending polling
|
|
292
295
|
cancelScrollRestorationPolling();
|
|
@@ -298,6 +301,11 @@ export function restoreScrollPosition(options?: {
|
|
|
298
301
|
return false;
|
|
299
302
|
}
|
|
300
303
|
|
|
304
|
+
// Once React committed the restored tree, scroll now, so a view
|
|
305
|
+
// transition's new snapshot is taken at the restored position, while the
|
|
306
|
+
// page still streams too.
|
|
307
|
+
if (options?.committed) window.scrollTo(0, savedY);
|
|
308
|
+
|
|
301
309
|
// If streaming, poll until streaming ends then scroll to saved position
|
|
302
310
|
if (options?.retryIfStreaming && options?.isStreaming?.()) {
|
|
303
311
|
const startTime = Date.now();
|
|
@@ -319,9 +327,8 @@ export function restoreScrollPosition(options?: {
|
|
|
319
327
|
return true;
|
|
320
328
|
}
|
|
321
329
|
|
|
322
|
-
// Not streaming
|
|
323
|
-
//
|
|
324
|
-
// would be overwritten when React replaces the content.
|
|
330
|
+
// Not streaming: scroll (again) on the next frame, for layout that settles
|
|
331
|
+
// after the commit and for a caller that ran before it.
|
|
325
332
|
deferToNextPaint(() => {
|
|
326
333
|
window.scrollTo(0, savedY);
|
|
327
334
|
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
@@ -374,18 +381,18 @@ export function handleNavigationStart(): void {
|
|
|
374
381
|
|
|
375
382
|
/**
|
|
376
383
|
* Handle scroll for a back/forward traversal, at popstate before anything
|
|
377
|
-
* renders: save the page
|
|
378
|
-
* so saveCurrentScrollPosition would use the destination's key)
|
|
379
|
-
*
|
|
384
|
+
* renders: save the page on screen under ITS key (history has already moved,
|
|
385
|
+
* so saveCurrentScrollPosition would use the destination's key). A second
|
|
386
|
+
* traversal before the first one's restore commits (a rapid double Back, a
|
|
387
|
+
* deferred animated restore) saves the page still on screen, never an entry
|
|
388
|
+
* it did not show.
|
|
380
389
|
*/
|
|
381
390
|
export function handleTraversalStart(): void {
|
|
382
391
|
if (!initialized) return;
|
|
383
|
-
const leftKey = displayedKey;
|
|
384
|
-
trackDisplayedEntry();
|
|
385
392
|
// A key shared with the destination (a getKey that collapses entries, e.g.
|
|
386
393
|
// by pathname) keeps the destination's saved position.
|
|
387
|
-
if (
|
|
388
|
-
saveScrollPosition(
|
|
394
|
+
if (displayedKey !== null && displayedKey !== getScrollKey()) {
|
|
395
|
+
saveScrollPosition(displayedKey, window.scrollY);
|
|
389
396
|
}
|
|
390
397
|
}
|
|
391
398
|
|
|
@@ -394,13 +401,15 @@ export function handleTraversalStart(): void {
|
|
|
394
401
|
* @param options.restore - If true, restore saved position (for popstate)
|
|
395
402
|
* @param options.scroll - If false, don't scroll at all
|
|
396
403
|
* @param options.isStreaming - Function to check if streaming is in progress (for retry logic)
|
|
404
|
+
* @param options.committed - Called after React committed the new tree
|
|
397
405
|
*/
|
|
398
406
|
export function handleNavigationEnd(options: {
|
|
399
407
|
restore?: boolean;
|
|
400
408
|
scroll?: boolean;
|
|
401
409
|
isStreaming?: () => boolean;
|
|
410
|
+
committed?: boolean;
|
|
402
411
|
}): void {
|
|
403
|
-
const { restore = false, scroll = true, isStreaming } = options;
|
|
412
|
+
const { restore = false, scroll = true, isStreaming, committed } = options;
|
|
404
413
|
|
|
405
414
|
// Don't scroll if explicitly disabled or not in a browser
|
|
406
415
|
if (scroll === false || typeof window === "undefined") {
|
|
@@ -412,11 +421,17 @@ export function handleNavigationEnd(options: {
|
|
|
412
421
|
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
413
422
|
// and remounts, creating a brief window where initialized is false.
|
|
414
423
|
if (restore) {
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
424
|
+
if (initialized) {
|
|
425
|
+
trackDisplayedEntry();
|
|
426
|
+
if (
|
|
427
|
+
restoreScrollPosition({
|
|
428
|
+
retryIfStreaming: true,
|
|
429
|
+
isStreaming,
|
|
430
|
+
committed,
|
|
431
|
+
})
|
|
432
|
+
) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
420
435
|
}
|
|
421
436
|
// An "auto" entry (no <Html.ScrollRestoration>, or created before it mounted)
|
|
422
437
|
// is restored by the browser. A scrollTo here races that restore, and
|
|
@@ -438,9 +453,8 @@ export function handleNavigationEnd(options: {
|
|
|
438
453
|
// against unmeasured layout, so sync scroll is correct here even
|
|
439
454
|
// before the new tree's scrollHeight settles.
|
|
440
455
|
//
|
|
441
|
-
// (The restore branch above
|
|
442
|
-
// depends on the new tree's max scroll
|
|
443
|
-
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
456
|
+
// (The restore branch above also scrolls again on the next frame: savedY
|
|
457
|
+
// depends on the new tree's max scroll, which can grow after the commit.)
|
|
444
458
|
if (scrollToHash()) {
|
|
445
459
|
return;
|
|
446
460
|
}
|