@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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/AGENTS.md +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
|
-
import
|
|
12
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
13
|
+
import type { TrackedActionState } from "../types.js";
|
|
13
14
|
import { invariant } from "../../errors.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -44,7 +45,7 @@ function normalizeActionId(actionId: string): string {
|
|
|
44
45
|
export function getActionId(action: ServerActionFunction | string): string {
|
|
45
46
|
invariant(
|
|
46
47
|
typeof action === "function" || typeof action === "string",
|
|
47
|
-
`useAction: action must be a function or string, got ${typeof action}
|
|
48
|
+
`useAction: action must be a function or string, got ${typeof action}`,
|
|
48
49
|
);
|
|
49
50
|
const actionId = (action as any)?.$$id;
|
|
50
51
|
if (actionId) {
|
|
@@ -72,7 +73,7 @@ Solutions:
|
|
|
72
73
|
2. Use the action name as a string:
|
|
73
74
|
const state = useAction("myAction");
|
|
74
75
|
|
|
75
|
-
The string must match the exported function name from your "use server" file
|
|
76
|
+
The string must match the exported function name from your "use server" file.`,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -126,19 +127,24 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
|
|
|
126
127
|
* const error = useAction(addToCart, state => state.error);
|
|
127
128
|
* ```
|
|
128
129
|
*
|
|
130
|
+
* @note The selector is expected to be stable for a given hook instance.
|
|
131
|
+
* This hook tracks one projection of one action. Changing selector semantics
|
|
132
|
+
* for the same action ID without a new action event is not a supported pattern;
|
|
133
|
+
* use separate useAction() subscriptions if you need different projections.
|
|
134
|
+
*
|
|
129
135
|
* @note Actions passed as props from server components lose their metadata
|
|
130
136
|
* during RSC serialization. Use a string action name or import directly.
|
|
131
137
|
*/
|
|
132
138
|
export function useAction(
|
|
133
|
-
action: ServerActionFunction | string
|
|
139
|
+
action: ServerActionFunction | string,
|
|
134
140
|
): TrackedActionState;
|
|
135
141
|
export function useAction<T>(
|
|
136
142
|
action: ServerActionFunction | string,
|
|
137
|
-
selector: (state: TrackedActionState) => T
|
|
143
|
+
selector: (state: TrackedActionState) => T,
|
|
138
144
|
): T;
|
|
139
145
|
export function useAction<T>(
|
|
140
146
|
action: ServerActionFunction | string,
|
|
141
|
-
selector?: (state: TrackedActionState) => T
|
|
147
|
+
selector?: (state: TrackedActionState) => T,
|
|
142
148
|
): T | TrackedActionState {
|
|
143
149
|
const ctx = useContext(NavigationStoreContext);
|
|
144
150
|
const actionId =
|
|
@@ -161,7 +167,10 @@ export function useAction<T>(
|
|
|
161
167
|
T | TrackedActionState
|
|
162
168
|
>(null!);
|
|
163
169
|
|
|
164
|
-
//
|
|
170
|
+
// Ref keeps the latest selector for subscription callbacks without
|
|
171
|
+
// re-subscribing on every render. Selector changes themselves are not
|
|
172
|
+
// treated as a reactive input; this hook expects a stable selector and
|
|
173
|
+
// represents one subscription/projection for one action.
|
|
165
174
|
const selectorRef = useRef(selector);
|
|
166
175
|
selectorRef.current = selector;
|
|
167
176
|
|
|
@@ -169,6 +178,17 @@ export function useAction<T>(
|
|
|
169
178
|
useEffect(() => {
|
|
170
179
|
if (!ctx) return;
|
|
171
180
|
|
|
181
|
+
// Sync current state for the (possibly new) actionId so that switching
|
|
182
|
+
// actions on an idle page doesn't leave stale data from the old action.
|
|
183
|
+
const currentState = ctx.eventController.getActionState(actionId);
|
|
184
|
+
const currentSelected = selectorRef.current
|
|
185
|
+
? selectorRef.current(currentState)
|
|
186
|
+
: currentState;
|
|
187
|
+
if (!shallowEqual(currentSelected, prevSelected.current)) {
|
|
188
|
+
prevSelected.current = currentSelected;
|
|
189
|
+
setBaseState(currentSelected);
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
// Subscribe to action-specific updates
|
|
173
193
|
const unsubscribe = ctx.eventController.subscribeToAction(
|
|
174
194
|
actionId,
|
|
@@ -177,14 +197,14 @@ export function useAction<T>(
|
|
|
177
197
|
? selectorRef.current(state)
|
|
178
198
|
: state;
|
|
179
199
|
|
|
180
|
-
if (!
|
|
200
|
+
if (!shallowEqual(selectedState, prevSelected.current)) {
|
|
181
201
|
prevSelected.current = selectedState;
|
|
182
202
|
setBaseState(selectedState);
|
|
183
203
|
startTransition(() => {
|
|
184
204
|
setOptimisticState(selectedState);
|
|
185
205
|
});
|
|
186
206
|
}
|
|
187
|
-
}
|
|
207
|
+
},
|
|
188
208
|
);
|
|
189
209
|
|
|
190
210
|
return () => {
|
|
@@ -195,46 +215,4 @@ export function useAction<T>(
|
|
|
195
215
|
return (optimisticState ?? baseState) as T | TrackedActionState;
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
function isShallowEqual<T, U>(selectedState: T, baseState: U): boolean {
|
|
199
|
-
// If references are equal, they're shallow equal
|
|
200
|
-
//@ts-expect-error -- TS doesn't like comparing generics
|
|
201
|
-
if (selectedState === baseState) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// If either is null/undefined and they're not equal, they're not shallow equal
|
|
206
|
-
if (selectedState == null || baseState == null) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If types are different, they're not shallow equal
|
|
211
|
-
if (typeof selectedState !== typeof baseState) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// For primitives, === comparison is sufficient (already checked above)
|
|
216
|
-
if (typeof selectedState !== "object") {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// For objects, compare keys and values shallowly
|
|
221
|
-
const keysA = Object.keys(selectedState as object);
|
|
222
|
-
const keysB = Object.keys(baseState as object);
|
|
223
|
-
|
|
224
|
-
if (keysA.length !== keysB.length) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const key of keysA) {
|
|
229
|
-
if (
|
|
230
|
-
!Object.prototype.hasOwnProperty.call(baseState, key) ||
|
|
231
|
-
(selectedState as any)[key] !== (baseState as any)[key]
|
|
232
|
-
) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
218
|
export type { TrackedActionState };
|
|
@@ -46,10 +46,12 @@ export interface ClientCacheControls {
|
|
|
46
46
|
export function useClientCache(): ClientCacheControls {
|
|
47
47
|
const ctx = useContext(NavigationStoreContext);
|
|
48
48
|
|
|
49
|
+
if (!ctx) {
|
|
50
|
+
throw new Error("useClientCache must be used within NavigationProvider");
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
const clear = useCallback(() => {
|
|
50
|
-
|
|
51
|
-
ctx.store.clearHistoryCache();
|
|
52
|
-
}
|
|
54
|
+
ctx.store.clearHistoryCache();
|
|
53
55
|
}, [ctx]);
|
|
54
56
|
|
|
55
57
|
return { clear };
|
|
@@ -9,125 +9,10 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { Handle } from "../../handle.js";
|
|
12
|
-
import {
|
|
12
|
+
import { collectHandleData } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* SSR module-level state.
|
|
18
|
-
* Populated by initHandleDataSync before React renders.
|
|
19
|
-
* Used by useState initializer during SSR.
|
|
20
|
-
*/
|
|
21
|
-
let ssrHandleData: HandleData = {};
|
|
22
|
-
let ssrSegmentOrder: string[] = [];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Filter segment IDs to only include routes and layouts.
|
|
26
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
27
|
-
*/
|
|
28
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
29
|
-
return matched.filter((id) => {
|
|
30
|
-
if (id.includes(".@")) return false;
|
|
31
|
-
if (/D\d+\./.test(id)) return false;
|
|
32
|
-
return true;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the collect function for a handle.
|
|
38
|
-
* When a handle is passed as a prop via RSC, toJSON strips the collect function.
|
|
39
|
-
* In that case, look up collect from the registry (populated when createHandle runs
|
|
40
|
-
* on the client), then fall back to flat array default.
|
|
41
|
-
*/
|
|
42
|
-
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
43
|
-
if (typeof handle.collect === "function") {
|
|
44
|
-
return handle.collect;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Handle was deserialized from RSC prop (toJSON stripped collect).
|
|
48
|
-
// Try the registry first (populated if the handle module was imported on client).
|
|
49
|
-
const registered = getCollectFn(handle.$$id);
|
|
50
|
-
if (registered) {
|
|
51
|
-
return registered as (segments: T[][]) => A;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Fall back to default flat collect with a dev warning.
|
|
55
|
-
if (process.env.NODE_ENV !== "production") {
|
|
56
|
-
console.warn(
|
|
57
|
-
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
58
|
-
`function could not be resolved. Falling back to flat array. ` +
|
|
59
|
-
`Import the handle module in a client component to register its collect function.`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return ((segments: unknown[][]) => segments.flat()) as unknown as (segments: T[][]) => A;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Collect handle data from segments and transform to final value.
|
|
67
|
-
*/
|
|
68
|
-
function collectHandle<T, A>(
|
|
69
|
-
handle: Handle<T, A>,
|
|
70
|
-
data: HandleData,
|
|
71
|
-
segmentOrder: string[]
|
|
72
|
-
): A {
|
|
73
|
-
const collect = resolveCollect(handle);
|
|
74
|
-
const segmentData = data[handle.$$id];
|
|
75
|
-
|
|
76
|
-
if (!segmentData) {
|
|
77
|
-
return collect([]);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Build array of segment arrays in parent -> child order
|
|
81
|
-
const segmentArrays: T[][] = [];
|
|
82
|
-
for (const segmentId of segmentOrder) {
|
|
83
|
-
const entries = segmentData[segmentId];
|
|
84
|
-
if (entries && entries.length > 0) {
|
|
85
|
-
segmentArrays.push(entries as T[]);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Call collect once with all segment data
|
|
90
|
-
return collect(segmentArrays);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Shallow equality check for selector results.
|
|
95
|
-
*/
|
|
96
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
97
|
-
if (Object.is(a, b)) return true;
|
|
98
|
-
if (
|
|
99
|
-
typeof a !== "object" ||
|
|
100
|
-
a === null ||
|
|
101
|
-
typeof b !== "object" ||
|
|
102
|
-
b === null
|
|
103
|
-
) {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
const keysA = Object.keys(a);
|
|
107
|
-
const keysB = Object.keys(b);
|
|
108
|
-
if (keysA.length !== keysB.length) return false;
|
|
109
|
-
for (const key of keysA) {
|
|
110
|
-
if (
|
|
111
|
-
!Object.hasOwn(b, key) ||
|
|
112
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
113
|
-
) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Initialize handle data synchronously for SSR.
|
|
122
|
-
* Called before rendering to populate state for useState initializer.
|
|
123
|
-
*
|
|
124
|
-
* @param data - Handle data from RSC payload
|
|
125
|
-
* @param matched - Segment order for reduction
|
|
126
|
-
*/
|
|
127
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
128
|
-
ssrHandleData = data;
|
|
129
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
130
|
-
}
|
|
15
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
131
16
|
|
|
132
17
|
/**
|
|
133
18
|
* Hook to access collected handle data.
|
|
@@ -150,25 +35,24 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
|
150
35
|
export function useHandle<T, A>(handle: Handle<T, A>): A;
|
|
151
36
|
export function useHandle<T, A, S>(
|
|
152
37
|
handle: Handle<T, A>,
|
|
153
|
-
selector: (data: A) => S
|
|
38
|
+
selector: (data: A) => S,
|
|
154
39
|
): S;
|
|
155
40
|
export function useHandle<T, A, S>(
|
|
156
41
|
handle: Handle<T, A>,
|
|
157
|
-
selector?: (data: A) => S
|
|
42
|
+
selector?: (data: A) => S,
|
|
158
43
|
): A | S {
|
|
159
44
|
const ctx = useContext(NavigationStoreContext);
|
|
160
45
|
|
|
161
|
-
// Initial state from
|
|
46
|
+
// Initial state from context event controller, or empty fallback without provider.
|
|
162
47
|
const [value, setValue] = useState<A | S>(() => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
|
|
48
|
+
if (!ctx) {
|
|
49
|
+
const collected = collectHandleData(handle, {}, []);
|
|
166
50
|
return selector ? selector(collected) : collected;
|
|
167
51
|
}
|
|
168
52
|
|
|
169
53
|
// On client, use event controller state
|
|
170
54
|
const state = ctx.eventController.getHandleState();
|
|
171
|
-
const collected =
|
|
55
|
+
const collected = collectHandleData(handle, state.data, state.segmentOrder);
|
|
172
56
|
return selector ? selector(collected) : collected;
|
|
173
57
|
});
|
|
174
58
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
@@ -177,7 +61,7 @@ export function useHandle<T, A, S>(
|
|
|
177
61
|
const prevValueRef = useRef(value);
|
|
178
62
|
prevValueRef.current = value;
|
|
179
63
|
|
|
180
|
-
//
|
|
64
|
+
// Ref keeps the latest selector without re-subscribing on every render.
|
|
181
65
|
const selectorRef = useRef(selector);
|
|
182
66
|
selectorRef.current = selector;
|
|
183
67
|
|
|
@@ -185,11 +69,31 @@ export function useHandle<T, A, S>(
|
|
|
185
69
|
useEffect(() => {
|
|
186
70
|
if (!ctx) return;
|
|
187
71
|
|
|
72
|
+
// Sync current state for the (possibly new) handle so that switching
|
|
73
|
+
// handles on an idle page doesn't leave stale data from the old handle.
|
|
74
|
+
const currentHandleState = ctx.eventController.getHandleState();
|
|
75
|
+
const currentCollected = collectHandleData(
|
|
76
|
+
handle,
|
|
77
|
+
currentHandleState.data,
|
|
78
|
+
currentHandleState.segmentOrder,
|
|
79
|
+
);
|
|
80
|
+
const currentValue = selectorRef.current
|
|
81
|
+
? selectorRef.current(currentCollected)
|
|
82
|
+
: currentCollected;
|
|
83
|
+
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
84
|
+
prevValueRef.current = currentValue;
|
|
85
|
+
setValue(currentValue);
|
|
86
|
+
}
|
|
87
|
+
|
|
188
88
|
return ctx.eventController.subscribeToHandles(() => {
|
|
189
89
|
const state = ctx.eventController.getHandleState();
|
|
190
90
|
const isAction =
|
|
191
91
|
ctx.eventController.getState().inflightActions.length > 0;
|
|
192
|
-
const collected =
|
|
92
|
+
const collected = collectHandleData(
|
|
93
|
+
handle,
|
|
94
|
+
state.data,
|
|
95
|
+
state.segmentOrder,
|
|
96
|
+
);
|
|
193
97
|
const nextValue = selectorRef.current
|
|
194
98
|
? selectorRef.current(collected)
|
|
195
99
|
: collected;
|
|
@@ -34,7 +34,7 @@ import { useMount } from "./use-mount.js";
|
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export function useHref(): (path:
|
|
37
|
+
export function useHref(): (path: `/${string}`) => string {
|
|
38
38
|
const mount = useMount();
|
|
39
|
-
return (path:
|
|
39
|
+
return (path: `/${string}`) => href(path as ValidPaths, mount);
|
|
40
40
|
}
|
|
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
|
|
|
16
16
|
* Context for Link component to provide its destination URL
|
|
17
17
|
* Used by useLinkStatus to determine if this specific link is pending
|
|
18
18
|
*/
|
|
19
|
-
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
19
|
+
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
20
|
+
null,
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Link status returned by useLinkStatus hook
|
|
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
|
|
|
46
48
|
function isPendingFor(
|
|
47
49
|
linkTo: string | null,
|
|
48
50
|
pendingUrl: string | null,
|
|
49
|
-
origin: string
|
|
51
|
+
origin: string,
|
|
50
52
|
): boolean {
|
|
51
53
|
if (linkTo === null || pendingUrl === null) {
|
|
52
54
|
return false;
|
|
@@ -81,9 +83,8 @@ export function useLinkStatus(): LinkStatus {
|
|
|
81
83
|
const ctx = useContext(NavigationStoreContext);
|
|
82
84
|
|
|
83
85
|
// Get origin for URL normalization (stable across renders)
|
|
84
|
-
const origin =
|
|
85
|
-
? window.location.origin
|
|
86
|
-
: "http://localhost";
|
|
86
|
+
const origin =
|
|
87
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
87
88
|
|
|
88
89
|
// Base state for useOptimistic
|
|
89
90
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
@@ -9,36 +9,10 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
|
-
import
|
|
12
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
13
|
+
import type { PublicNavigationState } from "../types.js";
|
|
13
14
|
import type { DerivedNavigationState } from "../event-controller.js";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Shallow equality check for selector results
|
|
17
|
-
*/
|
|
18
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
19
|
-
if (Object.is(a, b)) return true;
|
|
20
|
-
if (
|
|
21
|
-
typeof a !== "object" ||
|
|
22
|
-
a === null ||
|
|
23
|
-
typeof b !== "object" ||
|
|
24
|
-
b === null
|
|
25
|
-
) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const keysA = Object.keys(a);
|
|
29
|
-
const keysB = Object.keys(b);
|
|
30
|
-
if (keysA.length !== keysB.length) return false;
|
|
31
|
-
for (const key of keysA) {
|
|
32
|
-
if (
|
|
33
|
-
!Object.hasOwn(b, key) ||
|
|
34
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
35
|
-
) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
16
|
/**
|
|
43
17
|
* Convert derived state to public version (strips inflightActions)
|
|
44
18
|
*/
|
|
@@ -47,45 +21,29 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
|
|
|
47
21
|
return publicState;
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Navigation methods returned by useNavigation
|
|
53
|
-
*/
|
|
54
|
-
export interface NavigationMethods {
|
|
55
|
-
navigate: (url: string, options?: NavigateOptions) => Promise<void>;
|
|
56
|
-
refresh: () => Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
24
|
/**
|
|
60
|
-
*
|
|
61
|
-
*/
|
|
62
|
-
export type NavigationValue = PublicNavigationState & NavigationMethods;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Hook to access navigation state with optional selector for performance
|
|
25
|
+
* Hook to access reactive navigation state with optional selector for performance.
|
|
66
26
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
27
|
+
* Returns state only. For actions (push, replace, refresh, prefetch),
|
|
28
|
+
* use useRouter() instead.
|
|
69
29
|
*
|
|
70
30
|
* @example
|
|
71
31
|
* ```tsx
|
|
72
|
-
* const state = useNavigation(
|
|
32
|
+
* const { state, location } = useNavigation();
|
|
73
33
|
* const isLoading = useNavigation(nav => nav.state === 'loading');
|
|
74
34
|
* ```
|
|
75
35
|
*/
|
|
76
|
-
export function useNavigation():
|
|
36
|
+
export function useNavigation(): PublicNavigationState;
|
|
77
37
|
export function useNavigation<T>(
|
|
78
38
|
selector: (state: PublicNavigationState) => T,
|
|
79
39
|
): T;
|
|
80
40
|
export function useNavigation<T>(
|
|
81
41
|
selector?: (state: PublicNavigationState) => T,
|
|
82
|
-
): T |
|
|
42
|
+
): T | PublicNavigationState {
|
|
83
43
|
const ctx = useContext(NavigationStoreContext);
|
|
84
44
|
|
|
85
45
|
if (!ctx) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
"useNavigation must be used within NavigationStoreContext.Provider"
|
|
88
|
-
);
|
|
46
|
+
throw new Error("useNavigation must be used within NavigationProvider");
|
|
89
47
|
}
|
|
90
48
|
|
|
91
49
|
// Base state for useOptimistic
|
|
@@ -95,16 +53,32 @@ export function useNavigation<T>(
|
|
|
95
53
|
});
|
|
96
54
|
const prevState = useRef(baseValue);
|
|
97
55
|
|
|
56
|
+
// Tracks whether the most recent setOptimisticValue call pinned the value
|
|
57
|
+
// to a non-idle state. Used to decide whether to emit a release update when
|
|
58
|
+
// returning to idle, so the optimistic store doesn't stay pinned if a
|
|
59
|
+
// parent transition (e.g. <Link> click) is still pending.
|
|
60
|
+
const optimisticPinnedRef = useRef(false);
|
|
61
|
+
|
|
98
62
|
// useOptimistic allows immediate updates during transitions/actions
|
|
99
63
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
100
64
|
|
|
65
|
+
// Store selector in a ref so the subscription callback always uses the
|
|
66
|
+
// latest selector without re-subscribing on every render (inline functions
|
|
67
|
+
// have a new identity each render). This is event-driven by design: the
|
|
68
|
+
// value updates when the store emits, not when the selector changes.
|
|
69
|
+
// Between events there is nothing new to select from.
|
|
70
|
+
const selectorRef = useRef(selector);
|
|
71
|
+
selectorRef.current = selector;
|
|
72
|
+
|
|
101
73
|
// Subscribe to event controller state changes (only runs on client)
|
|
102
74
|
useEffect(() => {
|
|
103
75
|
// Subscribe to updates from event controller
|
|
104
76
|
return ctx.eventController.subscribe(() => {
|
|
105
77
|
const currentState = ctx.eventController.getState();
|
|
106
78
|
const publicState = toPublicState(currentState);
|
|
107
|
-
const nextSelected =
|
|
79
|
+
const nextSelected = selectorRef.current
|
|
80
|
+
? selectorRef.current(publicState)
|
|
81
|
+
: publicState;
|
|
108
82
|
|
|
109
83
|
// Check if selected value has changed
|
|
110
84
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
@@ -114,27 +88,32 @@ export function useNavigation<T>(
|
|
|
114
88
|
const hasInflightActions =
|
|
115
89
|
ctx.eventController.getInflightActions().size > 0;
|
|
116
90
|
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
119
97
|
startTransition(() => {
|
|
120
98
|
setOptimisticValue(nextSelected);
|
|
121
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;
|
|
122
110
|
}
|
|
123
111
|
|
|
124
112
|
// Always update base state so UI reflects current state
|
|
125
113
|
setBaseValue(nextSelected);
|
|
126
114
|
}
|
|
127
115
|
});
|
|
128
|
-
}, [
|
|
129
|
-
|
|
130
|
-
// If no selector, include navigation methods
|
|
131
|
-
if (!selector) {
|
|
132
|
-
return {
|
|
133
|
-
...(value as PublicNavigationState),
|
|
134
|
-
navigate: ctx.navigate,
|
|
135
|
-
refresh: ctx.refresh,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
116
|
+
}, []);
|
|
138
117
|
|
|
139
|
-
return value as T;
|
|
118
|
+
return value as T | PublicNavigationState;
|
|
140
119
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access the current route params.
|
|
9
|
+
*
|
|
10
|
+
* Returns the merged route params from the matched route.
|
|
11
|
+
* Updates when navigation completes, not during pending navigation.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // Route: /products/:productId
|
|
16
|
+
* const params = useParams();
|
|
17
|
+
* // { productId: "123" }
|
|
18
|
+
*
|
|
19
|
+
* // With selector
|
|
20
|
+
* const productId = useParams(p => p.productId);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useParams(): Record<string, string>;
|
|
24
|
+
export function useParams<T>(
|
|
25
|
+
selector: (params: Record<string, string>) => T,
|
|
26
|
+
): T;
|
|
27
|
+
export function useParams<T>(
|
|
28
|
+
selector?: (params: Record<string, string>) => T,
|
|
29
|
+
): T | Record<string, string> {
|
|
30
|
+
const ctx = useContext(NavigationStoreContext);
|
|
31
|
+
|
|
32
|
+
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
return selector ? selector({}) : {};
|
|
35
|
+
}
|
|
36
|
+
const params = ctx.eventController.getParams();
|
|
37
|
+
return selector ? selector(params) : params;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const prevValue = useRef(value);
|
|
41
|
+
// Ref keeps the latest selector without re-subscribing. Event-driven by
|
|
42
|
+
// design: value updates on store events, not on selector identity change.
|
|
43
|
+
const selectorRef = useRef(selector);
|
|
44
|
+
selectorRef.current = selector;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!ctx) return;
|
|
48
|
+
|
|
49
|
+
const update = () => {
|
|
50
|
+
const params = ctx.eventController.getParams();
|
|
51
|
+
const next = selectorRef.current ? selectorRef.current(params) : params;
|
|
52
|
+
|
|
53
|
+
if (!shallowEqual(next, prevValue.current)) {
|
|
54
|
+
prevValue.current = next;
|
|
55
|
+
setValue(next);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
update();
|
|
60
|
+
|
|
61
|
+
return ctx.eventController.subscribe(update);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to access the current pathname.
|
|
8
|
+
*
|
|
9
|
+
* Returns the committed pathname string (excludes search params and hash).
|
|
10
|
+
* Updates when navigation completes, not during pending navigation.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const pathname = usePathname();
|
|
15
|
+
* // "/products/123"
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function usePathname(): string {
|
|
19
|
+
const ctx = useContext(NavigationStoreContext);
|
|
20
|
+
|
|
21
|
+
const [pathname, setPathname] = useState<string>(() => {
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
return "/";
|
|
24
|
+
}
|
|
25
|
+
return (ctx.eventController.getState().location as URL).pathname;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const prevPathname = useRef(pathname);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!ctx) return;
|
|
32
|
+
|
|
33
|
+
const update = () => {
|
|
34
|
+
const next = (ctx.eventController.getState().location as URL).pathname;
|
|
35
|
+
if (next !== prevPathname.current) {
|
|
36
|
+
prevPathname.current = next;
|
|
37
|
+
setPathname(next);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
update();
|
|
42
|
+
|
|
43
|
+
return ctx.eventController.subscribe(update);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return pathname;
|
|
47
|
+
}
|