@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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 +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -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/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -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 +37 -0
- 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 +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- 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 +63 -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 +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -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 +411 -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 +469 -0
- package/src/build/route-types/scan-filter.ts +78 -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 +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -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 +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- 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 +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -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 +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -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 +289 -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 +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -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 +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -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 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -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 +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -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 +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- 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/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -9,27 +9,34 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { Handle } from "../../handle.js";
|
|
12
|
+
import { getCollectFn } from "../../handle.js";
|
|
12
13
|
import type { HandleData } from "../types.js";
|
|
13
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Resolve the collect function for a handle.
|
|
19
|
+
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
20
|
+
* (populated when createHandle runs on the client).
|
|
19
21
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
23
|
+
// Look up collect from the registry (populated when the handle module is imported).
|
|
24
|
+
const registered = getCollectFn(handle.$$id);
|
|
25
|
+
if (registered) {
|
|
26
|
+
return registered as (segments: T[][]) => A;
|
|
27
|
+
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// Fall back to default flat collect with a dev warning.
|
|
30
|
+
if (process.env.NODE_ENV !== "production") {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
33
|
+
`function could not be resolved. Falling back to flat array. ` +
|
|
34
|
+
`Import the handle module in a client component to register its collect function.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return ((segments: unknown[][]) => segments.flat()) as unknown as (
|
|
38
|
+
segments: T[][],
|
|
39
|
+
) => A;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
/**
|
|
@@ -38,15 +45,16 @@ function filterSegmentOrder(matched: string[]): string[] {
|
|
|
38
45
|
function collectHandle<T, A>(
|
|
39
46
|
handle: Handle<T, A>,
|
|
40
47
|
data: HandleData,
|
|
41
|
-
segmentOrder: string[]
|
|
48
|
+
segmentOrder: string[],
|
|
42
49
|
): A {
|
|
50
|
+
const collect = resolveCollect(handle);
|
|
43
51
|
const segmentData = data[handle.$$id];
|
|
44
52
|
|
|
45
53
|
if (!segmentData) {
|
|
46
|
-
return
|
|
54
|
+
return collect([]);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
// Build array of segment arrays in parent
|
|
57
|
+
// Build array of segment arrays in parent -> child order
|
|
50
58
|
const segmentArrays: T[][] = [];
|
|
51
59
|
for (const segmentId of segmentOrder) {
|
|
52
60
|
const entries = segmentData[segmentId];
|
|
@@ -56,46 +64,7 @@ function collectHandle<T, A>(
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Call collect once with all segment data
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Shallow equality check for selector results.
|
|
64
|
-
*/
|
|
65
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
66
|
-
if (Object.is(a, b)) return true;
|
|
67
|
-
if (
|
|
68
|
-
typeof a !== "object" ||
|
|
69
|
-
a === null ||
|
|
70
|
-
typeof b !== "object" ||
|
|
71
|
-
b === null
|
|
72
|
-
) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
const keysA = Object.keys(a);
|
|
76
|
-
const keysB = Object.keys(b);
|
|
77
|
-
if (keysA.length !== keysB.length) return false;
|
|
78
|
-
for (const key of keysA) {
|
|
79
|
-
if (
|
|
80
|
-
!Object.hasOwn(b, key) ||
|
|
81
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
82
|
-
) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Initialize handle data synchronously for SSR.
|
|
91
|
-
* Called before rendering to populate state for useState initializer.
|
|
92
|
-
*
|
|
93
|
-
* @param data - Handle data from RSC payload
|
|
94
|
-
* @param matched - Segment order for reduction
|
|
95
|
-
*/
|
|
96
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
97
|
-
ssrHandleData = data;
|
|
98
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
67
|
+
return collect(segmentArrays);
|
|
99
68
|
}
|
|
100
69
|
|
|
101
70
|
/**
|
|
@@ -119,19 +88,18 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
|
119
88
|
export function useHandle<T, A>(handle: Handle<T, A>): A;
|
|
120
89
|
export function useHandle<T, A, S>(
|
|
121
90
|
handle: Handle<T, A>,
|
|
122
|
-
selector: (data: A) => S
|
|
91
|
+
selector: (data: A) => S,
|
|
123
92
|
): S;
|
|
124
93
|
export function useHandle<T, A, S>(
|
|
125
94
|
handle: Handle<T, A>,
|
|
126
|
-
selector?: (data: A) => S
|
|
95
|
+
selector?: (data: A) => S,
|
|
127
96
|
): A | S {
|
|
128
97
|
const ctx = useContext(NavigationStoreContext);
|
|
129
98
|
|
|
130
|
-
// Initial state from
|
|
99
|
+
// Initial state from context event controller, or empty fallback without provider.
|
|
131
100
|
const [value, setValue] = useState<A | S>(() => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
|
|
101
|
+
if (!ctx) {
|
|
102
|
+
const collected = collectHandle(handle, {}, []);
|
|
135
103
|
return selector ? selector(collected) : collected;
|
|
136
104
|
}
|
|
137
105
|
|
|
@@ -146,7 +114,7 @@ export function useHandle<T, A, S>(
|
|
|
146
114
|
const prevValueRef = useRef(value);
|
|
147
115
|
prevValueRef.current = value;
|
|
148
116
|
|
|
149
|
-
//
|
|
117
|
+
// Ref keeps the latest selector without re-subscribing on every render.
|
|
150
118
|
const selectorRef = useRef(selector);
|
|
151
119
|
selectorRef.current = selector;
|
|
152
120
|
|
|
@@ -154,6 +122,22 @@ export function useHandle<T, A, S>(
|
|
|
154
122
|
useEffect(() => {
|
|
155
123
|
if (!ctx) return;
|
|
156
124
|
|
|
125
|
+
// Sync current state for the (possibly new) handle so that switching
|
|
126
|
+
// handles on an idle page doesn't leave stale data from the old handle.
|
|
127
|
+
const currentHandleState = ctx.eventController.getHandleState();
|
|
128
|
+
const currentCollected = collectHandle(
|
|
129
|
+
handle,
|
|
130
|
+
currentHandleState.data,
|
|
131
|
+
currentHandleState.segmentOrder,
|
|
132
|
+
);
|
|
133
|
+
const currentValue = selectorRef.current
|
|
134
|
+
? selectorRef.current(currentCollected)
|
|
135
|
+
: currentCollected;
|
|
136
|
+
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
137
|
+
prevValueRef.current = currentValue;
|
|
138
|
+
setValue(currentValue);
|
|
139
|
+
}
|
|
140
|
+
|
|
157
141
|
return ctx.eventController.subscribeToHandles(() => {
|
|
158
142
|
const state = ctx.eventController.getHandleState();
|
|
159
143
|
const isAction =
|
|
@@ -1,208 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type { UrlPatterns } from "../../urls.js";
|
|
6
|
-
import { HrefContext, type HrefContextValue } from "../../href-context.js";
|
|
7
|
-
|
|
8
|
-
// Re-export for backwards compatibility
|
|
9
|
-
export { HrefContext, type HrefContextValue } from "../../href-context.js";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Resolution priority for href:
|
|
13
|
-
* 1. Path-based (/blog/:slug) → Use directly
|
|
14
|
-
* 2. Absolute name (shop.cart) → Global lookup (has dot separator)
|
|
15
|
-
* 3. Local name (index) → Prepend current name prefix, then lookup
|
|
16
|
-
*/
|
|
17
|
-
function resolveRouteName(
|
|
18
|
-
name: string,
|
|
19
|
-
routeMap: Record<string, string>,
|
|
20
|
-
currentRoutePrefix?: string
|
|
21
|
-
): string | undefined {
|
|
22
|
-
// 1. Path-based - starts with /
|
|
23
|
-
if (name.startsWith("/")) {
|
|
24
|
-
return name;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 2. Absolute name - already has a dot (e.g., "shop.cart")
|
|
28
|
-
if (name.includes(".")) {
|
|
29
|
-
return routeMap[name];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 3. Local name - try with current prefix first, then fall back to direct lookup
|
|
33
|
-
if (currentRoutePrefix) {
|
|
34
|
-
// Extract the prefix from current route name
|
|
35
|
-
// e.g., "blog.posts.detail" → prefix is "blog.posts"
|
|
36
|
-
const lastDot = currentRoutePrefix.lastIndexOf(".");
|
|
37
|
-
const prefix = lastDot > 0 ? currentRoutePrefix.substring(0, lastDot) : currentRoutePrefix;
|
|
38
|
-
|
|
39
|
-
// Try prefixed name
|
|
40
|
-
const prefixedName = `${prefix}.${name}`;
|
|
41
|
-
if (routeMap[prefixedName] !== undefined) {
|
|
42
|
-
return routeMap[prefixedName];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// If current route is a nested include, try parent prefixes
|
|
46
|
-
// e.g., for "blog.posts.detail", try "blog.posts.index", then "blog.index"
|
|
47
|
-
let currentPrefix = prefix;
|
|
48
|
-
while (currentPrefix.includes(".")) {
|
|
49
|
-
const parentDot = currentPrefix.lastIndexOf(".");
|
|
50
|
-
currentPrefix = currentPrefix.substring(0, parentDot);
|
|
51
|
-
const parentPrefixedName = `${currentPrefix}.${name}`;
|
|
52
|
-
if (routeMap[parentPrefixedName] !== undefined) {
|
|
53
|
-
return routeMap[parentPrefixedName];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Fall back to direct lookup (route without prefix)
|
|
59
|
-
return routeMap[name];
|
|
60
|
-
}
|
|
3
|
+
import { href, type ValidPaths } from "../../href-client.js";
|
|
4
|
+
import { useMount } from "./use-mount.js";
|
|
61
5
|
|
|
62
6
|
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
* @param name - Route name (local or absolute) or path-based URL
|
|
70
|
-
* @param params - Optional params for dynamic segments
|
|
71
|
-
* @returns The resolved URL
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* const href = useHref();
|
|
76
|
-
*
|
|
77
|
-
* // Local name (resolved with current prefix)
|
|
78
|
-
* href("index") // → "/blog" (if inside blog patterns)
|
|
79
|
-
* href("post", { slug: "hello" }) // → "/blog/hello"
|
|
80
|
-
*
|
|
81
|
-
* // Absolute name (direct lookup)
|
|
82
|
-
* href("shop.cart") // → "/shop/cart"
|
|
83
|
-
*
|
|
84
|
-
* // Path-based (used directly)
|
|
85
|
-
* href("/about") // → "/about"
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
(name: string, params?: Record<string, string>): string;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Client-side hook for resolving route names with current name prefix.
|
|
7
|
+
* Client-side hook for mount-aware URL resolution.
|
|
8
|
+
*
|
|
9
|
+
* Returns an href function that automatically prepends the current
|
|
10
|
+
* include() mount prefix. Inside an include("/shop", ...) scope,
|
|
11
|
+
* the returned function resolves local paths relative to /shop.
|
|
93
12
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* 2. Absolute name (`shop.cart`) → Global lookup (contains dot)
|
|
97
|
-
* 3. Local name (`index`) → Prepend current name prefix, then lookup
|
|
13
|
+
* For absolute paths (outside the current mount), use the bare
|
|
14
|
+
* href() function directly instead.
|
|
98
15
|
*
|
|
99
|
-
* @
|
|
100
|
-
* @returns A function to generate URLs from route names
|
|
16
|
+
* @returns A function that prepends the mount prefix to paths
|
|
101
17
|
*
|
|
102
18
|
* @example
|
|
103
19
|
* ```tsx
|
|
104
20
|
* "use client";
|
|
105
|
-
* import { useHref } from "@rangojs/router/client";
|
|
106
|
-
* import { blogPatterns } from "../urls/blog";
|
|
21
|
+
* import { useHref, href } from "@rangojs/router/client";
|
|
107
22
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* const href = useHref
|
|
23
|
+
* // Inside include("/shop", shopPatterns)
|
|
24
|
+
* function ShopNav() {
|
|
25
|
+
* const href = useHref();
|
|
111
26
|
*
|
|
112
27
|
* return (
|
|
113
28
|
* <>
|
|
114
|
-
* {
|
|
115
|
-
* <Link
|
|
116
|
-
* <Link
|
|
117
|
-
*
|
|
118
|
-
* {/* Absolute names - explicit prefix *\/}
|
|
119
|
-
* <Link href={href("shop.cart")}>Cart</Link>
|
|
120
|
-
*
|
|
121
|
-
* {/* Path-based - always works *\/}
|
|
122
|
-
* <Link href={href("/about")}>About</Link>
|
|
29
|
+
* {// Local paths - auto-prefixed with /shop}
|
|
30
|
+
* <Link to={href("/cart")}>Cart</Link>
|
|
31
|
+
* <Link to={href("/product/widget")}>Widget</Link>
|
|
123
32
|
* </>
|
|
124
33
|
* );
|
|
125
34
|
* }
|
|
126
35
|
* ```
|
|
127
36
|
*/
|
|
128
|
-
export function useHref
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
? ScopedHrefFunction<TRoutes>
|
|
132
|
-
: HrefFn {
|
|
133
|
-
const context = useContext(HrefContext);
|
|
134
|
-
|
|
135
|
-
if (!context) {
|
|
136
|
-
// Return a function that warns and returns the name as-is
|
|
137
|
-
return ((name: string, _params?: Record<string, string>) => {
|
|
138
|
-
if (process.env.NODE_ENV !== "production") {
|
|
139
|
-
console.warn(
|
|
140
|
-
"[useHref] HrefContext not found. Make sure HrefProvider is mounted. Returning name as-is."
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return name;
|
|
144
|
-
}) as any;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const { routeMap, routeName } = context;
|
|
148
|
-
|
|
149
|
-
return ((name: string, params?: Record<string, string>) => {
|
|
150
|
-
// Path-based - return directly (optionally with param substitution)
|
|
151
|
-
if (name.startsWith("/")) {
|
|
152
|
-
if (params) {
|
|
153
|
-
// Substitute params in path-based URL
|
|
154
|
-
return name.replace(/:([^/]+)/g, (_, key) => {
|
|
155
|
-
const value = params[key];
|
|
156
|
-
if (value === undefined) {
|
|
157
|
-
throw new Error(`Missing param "${key}" for path "${name}"`);
|
|
158
|
-
}
|
|
159
|
-
return encodeURIComponent(value);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
return name;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Resolve route name
|
|
166
|
-
const pattern = resolveRouteName(name, routeMap, routeName);
|
|
167
|
-
|
|
168
|
-
if (pattern === undefined) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Unknown route: "${name}"${routeName ? ` (current route: ${routeName})` : ""}`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If no params, return pattern directly
|
|
175
|
-
if (!params) {
|
|
176
|
-
return pattern;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Substitute params
|
|
180
|
-
return pattern.replace(/:([^/]+)/g, (_, key) => {
|
|
181
|
-
const value = params[key];
|
|
182
|
-
if (value === undefined) {
|
|
183
|
-
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
184
|
-
}
|
|
185
|
-
return encodeURIComponent(value);
|
|
186
|
-
});
|
|
187
|
-
}) as any;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Provider component for href context
|
|
192
|
-
* Used internally by NavigationProvider to pass route map from RSC metadata
|
|
193
|
-
*/
|
|
194
|
-
export function HrefProvider({
|
|
195
|
-
routeMap,
|
|
196
|
-
routeName,
|
|
197
|
-
children,
|
|
198
|
-
}: {
|
|
199
|
-
routeMap: Record<string, string>;
|
|
200
|
-
routeName?: string;
|
|
201
|
-
children: React.ReactNode;
|
|
202
|
-
}): React.ReactElement {
|
|
203
|
-
return (
|
|
204
|
-
<HrefContext.Provider value={{ routeMap, routeName }}>
|
|
205
|
-
{children}
|
|
206
|
-
</HrefContext.Provider>
|
|
207
|
-
);
|
|
37
|
+
export function useHref(): (path: `/${string}`) => string {
|
|
38
|
+
const mount = useMount();
|
|
39
|
+
return (path: `/${string}`) => href(path as ValidPaths, mount);
|
|
208
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>(() => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { MountContext } from "./mount-context.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the current include() mount path.
|
|
8
|
+
*
|
|
9
|
+
* Inside `include("/articles", blogPatterns)`, returns "/articles".
|
|
10
|
+
* For nested includes, returns the nearest mount path.
|
|
11
|
+
* At root level (no include), returns "/".
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* "use client";
|
|
16
|
+
* import { useMount, href } from "@rangojs/router/client";
|
|
17
|
+
*
|
|
18
|
+
* function BlogNav({ slug }: { slug: string }) {
|
|
19
|
+
* const mount = useMount(); // "/articles"
|
|
20
|
+
* return (
|
|
21
|
+
* <>
|
|
22
|
+
* <Link to={href("/", mount)}>Blog Home</Link>
|
|
23
|
+
* <Link to={href(`/${slug}`, mount)}>Post</Link>
|
|
24
|
+
* </>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useMount(): string {
|
|
30
|
+
return useContext(MountContext);
|
|
31
|
+
}
|
|
@@ -9,44 +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
|
-
// SSR-safe default state (public version without internal properties)
|
|
43
|
-
const SSR_DEFAULT_STATE: PublicNavigationState = {
|
|
44
|
-
state: "idle",
|
|
45
|
-
isStreaming: false,
|
|
46
|
-
location: new URL("/", "http://localhost"),
|
|
47
|
-
pendingUrl: null,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
16
|
/**
|
|
51
17
|
* Convert derived state to public version (strips inflightActions)
|
|
52
18
|
*/
|
|
@@ -55,49 +21,33 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
|
|
|
55
21
|
return publicState;
|
|
56
22
|
}
|
|
57
23
|
|
|
58
|
-
// No-op functions for SSR
|
|
59
|
-
const noopNavigate = async () => {};
|
|
60
|
-
const noopRefresh = async () => {};
|
|
61
|
-
|
|
62
24
|
/**
|
|
63
|
-
*
|
|
64
|
-
*/
|
|
65
|
-
export interface NavigationMethods {
|
|
66
|
-
navigate: (url: string, options?: NavigateOptions) => Promise<void>;
|
|
67
|
-
refresh: () => Promise<void>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Full value returned when no selector is provided
|
|
72
|
-
*/
|
|
73
|
-
export type NavigationValue = PublicNavigationState & NavigationMethods;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Hook to access navigation state with optional selector for performance
|
|
25
|
+
* Hook to access reactive navigation state with optional selector for performance.
|
|
77
26
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
27
|
+
* Returns state only. For actions (push, replace, refresh, prefetch),
|
|
28
|
+
* use useRouter() instead.
|
|
80
29
|
*
|
|
81
30
|
* @example
|
|
82
31
|
* ```tsx
|
|
83
|
-
* const state = useNavigation(
|
|
32
|
+
* const { state, location } = useNavigation();
|
|
84
33
|
* const isLoading = useNavigation(nav => nav.state === 'loading');
|
|
85
34
|
* ```
|
|
86
35
|
*/
|
|
87
|
-
export function useNavigation():
|
|
36
|
+
export function useNavigation(): PublicNavigationState;
|
|
88
37
|
export function useNavigation<T>(
|
|
89
|
-
selector: (state: PublicNavigationState) => T
|
|
38
|
+
selector: (state: PublicNavigationState) => T,
|
|
90
39
|
): T;
|
|
91
40
|
export function useNavigation<T>(
|
|
92
|
-
selector?: (state: PublicNavigationState) => T
|
|
93
|
-
): T |
|
|
41
|
+
selector?: (state: PublicNavigationState) => T,
|
|
42
|
+
): T | PublicNavigationState {
|
|
94
43
|
const ctx = useContext(NavigationStoreContext);
|
|
95
44
|
|
|
45
|
+
if (!ctx) {
|
|
46
|
+
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
|
+
}
|
|
48
|
+
|
|
96
49
|
// Base state for useOptimistic
|
|
97
50
|
const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
|
|
98
|
-
if (typeof document === "undefined" || !ctx) {
|
|
99
|
-
return selector ? selector(SSR_DEFAULT_STATE) : SSR_DEFAULT_STATE;
|
|
100
|
-
}
|
|
101
51
|
const publicState = toPublicState(ctx.eventController.getState());
|
|
102
52
|
return selector ? selector(publicState) : publicState;
|
|
103
53
|
});
|
|
@@ -106,15 +56,23 @@ export function useNavigation<T>(
|
|
|
106
56
|
// useOptimistic allows immediate updates during transitions/actions
|
|
107
57
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
108
58
|
|
|
59
|
+
// Store selector in a ref so the subscription callback always uses the
|
|
60
|
+
// latest selector without re-subscribing on every render (inline functions
|
|
61
|
+
// have a new identity each render). This is event-driven by design: the
|
|
62
|
+
// value updates when the store emits, not when the selector changes.
|
|
63
|
+
// Between events there is nothing new to select from.
|
|
64
|
+
const selectorRef = useRef(selector);
|
|
65
|
+
selectorRef.current = selector;
|
|
66
|
+
|
|
109
67
|
// Subscribe to event controller state changes (only runs on client)
|
|
110
68
|
useEffect(() => {
|
|
111
|
-
if (!ctx) return;
|
|
112
|
-
|
|
113
69
|
// Subscribe to updates from event controller
|
|
114
70
|
return ctx.eventController.subscribe(() => {
|
|
115
71
|
const currentState = ctx.eventController.getState();
|
|
116
72
|
const publicState = toPublicState(currentState);
|
|
117
|
-
const nextSelected =
|
|
73
|
+
const nextSelected = selectorRef.current
|
|
74
|
+
? selectorRef.current(publicState)
|
|
75
|
+
: publicState;
|
|
118
76
|
|
|
119
77
|
// Check if selected value has changed
|
|
120
78
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
@@ -135,16 +93,7 @@ export function useNavigation<T>(
|
|
|
135
93
|
setBaseValue(nextSelected);
|
|
136
94
|
}
|
|
137
95
|
});
|
|
138
|
-
}, [
|
|
139
|
-
|
|
140
|
-
// If no selector, include navigation methods
|
|
141
|
-
if (!selector) {
|
|
142
|
-
return {
|
|
143
|
-
...(value as PublicNavigationState),
|
|
144
|
-
navigate: ctx?.navigate ?? noopNavigate,
|
|
145
|
-
refresh: ctx?.refresh ?? noopRefresh,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
96
|
+
}, []);
|
|
148
97
|
|
|
149
|
-
return value as T;
|
|
98
|
+
return value as T | PublicNavigationState;
|
|
150
99
|
}
|
|
@@ -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
|
+
}
|