Skip to content

Commit 3141ffa

Browse files
author
mike.duminy
committed
simplify useSignalSelector API, do not require a wrapping Provider
1 parent affd148 commit 3141ffa

3 files changed

Lines changed: 117 additions & 81 deletions

File tree

src/signals/context.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ export interface SignalContextValue<
1515

1616
export { ReactReduxContext }
1717

18-
export function useSignalContext<S>(): SignalContextValue<S> {
18+
export function useSignalContext<S>(
19+
context?: React.Context<SignalContextValue<S>>,
20+
): SignalContextValue<S> {
1921
const contextValue = React.useContext(
20-
ReactReduxContext as React.Context<SignalContextValue<S> | null>,
22+
(context ||
23+
ReactReduxContext) as React.Context<SignalContextValue<S> | null>,
2124
)
2225

2326
if (!contextValue) {
24-
throw new Error(
25-
'useSignalSelector must be used within a <SignalProvider>',
26-
)
27+
throw new Error('useSignalSelector must be used within a <SignalProvider>')
2728
}
2829

2930
// Verify this is actually a signal context (has registry + engine)

src/signals/useSignalSelector.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { React } from '../utils/react'
22
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
3+
import type { SignalContextValue } from './context'
34
import { useSignalContext } from './context'
5+
import { reconcileState } from './diff'
46
import {
57
createTrackingProxy,
68
getProxyPath,
@@ -9,7 +11,6 @@ import {
911

1012
const { useRef, useMemo, useEffect, useSyncExternalStore } = React
1113

12-
1314
/**
1415
* A React hook that selects state from a Redux store using signal-based
1516
* dependency tracking. Only re-renders when the selected value actually
@@ -18,13 +19,33 @@ const { useRef, useMemo, useEffect, useSyncExternalStore } = React
1819
* Must be used within a <SignalProvider>.
1920
* @param selector - Function that extracts a value from the store state
2021
* @param equalityFn - Custom equality function for change detection
22+
* @param context - The signal context containing store, registry, and engine
2123
* @returns The selected value
2224
*/
2325
export function useSignalSelector<S extends object, R>(
2426
selector: (state: S) => R,
2527
equalityFn: (a: R, b: R) => boolean = Object.is,
28+
context?: React.Context<SignalContextValue<S>>,
2629
): R {
27-
const { store, registry, engine } = useSignalContext<S>()
30+
const { store, registry, engine, subscription } = useSignalContext<S>(context)
31+
32+
const prevStateRef = React.useRef<S>(store.getState())
33+
34+
// Standard Provider subscription logic (from components/Provider.tsx)
35+
// + signal diff on each dispatch
36+
useIsomorphicLayoutEffect(() => {
37+
subscription.onStateChange = () => {
38+
// Run signal diff BEFORE notifying nested subs, so computed values
39+
// are up-to-date when useSelector/useSignalSelector read them
40+
const prev = prevStateRef.current
41+
const next = store.getState()
42+
prevStateRef.current = next
43+
reconcileState(prev, next, registry, engine)
44+
45+
subscription.notifyNestedSubs()
46+
}
47+
subscription.trySubscribe()
48+
}, [subscription])
2849

2950
// Track latest selector/equalityFn via refs, synced in layout effect (not during render)
3051
const selectorRef = useRef(selector)
@@ -146,3 +167,12 @@ export function useSignalSelector<S extends object, R>(
146167

147168
return useSyncExternalStore(bridge.subscribe, bridge.getSnapshot)
148169
}
170+
171+
export const createUseSignalSelector = (
172+
context?: React.Context<SignalContextValue<any>>,
173+
) => {
174+
return <S extends object, R>(
175+
selector: (state: S) => R,
176+
equalityFn?: (a: R, b: R) => boolean,
177+
) => useSignalSelector(selector, equalityFn, context)
178+
}

0 commit comments

Comments
 (0)