Skip to content

Commit dc02c71

Browse files
committed
Rewrite useSelector with concurrent compat and wrapper behavior
1 parent fb153ff commit dc02c71

File tree

1 file changed

+113
-8
lines changed

1 file changed

+113
-8
lines changed

src/hooks/useSelector.ts

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
useReduxContext as useDefaultReduxContext,
1010
} from './useReduxContext'
1111

12+
import { experimental } from 'react-concurrent-store'
13+
const { useStoreSelector } = experimental
14+
1215
/**
1316
* The frequency of development mode checks.
1417
*
@@ -161,10 +164,109 @@ export function createSelectorHook(
161164

162165
const reduxContext = useReduxContext()
163166

164-
const { store, subscription, getServerState } = reduxContext
167+
const { store, reactStore, subscription, getServerState } = reduxContext
165168

169+
const selectorRef = React.useRef(selector)
170+
const equalityFnRef = React.useRef(equalityFn)
171+
const lastResultRef = React.useRef<Selected>(null)
166172
const firstRun = React.useRef(true)
167173

174+
// Update refs on each render
175+
React.useLayoutEffect(() => {
176+
selectorRef.current = selector
177+
equalityFnRef.current = equalityFn
178+
})
179+
180+
// Create stable selector wrapper
181+
const stableSelector = React.useCallback((state: TState): Selected => {
182+
const selected = selectorRef.current(state)
183+
184+
// Dev mode checks
185+
if (process.env.NODE_ENV !== 'production') {
186+
const { devModeChecks = {} } =
187+
typeof equalityFnOrOptions === 'function' ? {} : equalityFnOrOptions
188+
const { identityFunctionCheck, stabilityCheck } = reduxContext
189+
const {
190+
identityFunctionCheck: finalIdentityFunctionCheck,
191+
stabilityCheck: finalStabilityCheck,
192+
} = {
193+
stabilityCheck,
194+
identityFunctionCheck,
195+
...devModeChecks,
196+
}
197+
198+
// Stability check
199+
if (
200+
finalStabilityCheck === 'always' ||
201+
(finalStabilityCheck === 'once' && firstRun.current)
202+
) {
203+
const toCompare = selectorRef.current(state)
204+
if (!equalityFnRef.current(selected, toCompare)) {
205+
let stack: string | undefined = undefined
206+
try {
207+
throw new Error()
208+
} catch (e) {
209+
;({ stack } = e as Error)
210+
}
211+
console.warn(
212+
'Selector ' +
213+
(selector.name || 'unknown') +
214+
' returned a different result when called with the same parameters. ' +
215+
'This can lead to unnecessary rerenders.' +
216+
'\nSelectors that return a new reference (such as an object or an array) ' +
217+
'should be memoized: https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization',
218+
{
219+
state,
220+
selected,
221+
selected2: toCompare,
222+
stack,
223+
},
224+
)
225+
}
226+
}
227+
228+
// Identity function check
229+
if (
230+
finalIdentityFunctionCheck === 'always' ||
231+
(finalIdentityFunctionCheck === 'once' && firstRun.current)
232+
) {
233+
// @ts-ignore
234+
if (selected === state) {
235+
let stack: string | undefined = undefined
236+
try {
237+
throw new Error()
238+
} catch (e) {
239+
// eslint-disable-next-line no-extra-semi
240+
;({ stack } = e as Error)
241+
}
242+
console.warn(
243+
'Selector ' +
244+
(selector.name || 'unknown') +
245+
' returned the root state when called. This can lead to unnecessary rerenders.' +
246+
'\nSelectors that return the entire state are almost certainly a mistake, as they will cause a rerender whenever *anything* in state changes.',
247+
{ stack },
248+
)
249+
}
250+
}
251+
252+
if (firstRun.current) firstRun.current = false
253+
}
254+
255+
// Apply equality function
256+
if (
257+
lastResultRef.current !== undefined &&
258+
// @ts-ignore
259+
equalityFnRef.current(lastResultRef.current, selected)
260+
) {
261+
// @ts-ignore
262+
return lastResultRef.current
263+
}
264+
265+
lastResultRef.current = selected
266+
return selected
267+
}, []) // Empty deps - stable forever
268+
269+
/*
168270
const wrappedSelector = React.useCallback<typeof selector>(
169271
{
170272
[selector.name](state: TState) {
@@ -239,14 +341,17 @@ export function createSelectorHook(
239341
}[selector.name],
240342
[selector],
241343
)
344+
*/
242345

243-
const selectedState = useSyncExternalStoreWithSelector(
244-
subscription.addNestedSub,
245-
store.getState,
246-
getServerState || store.getState,
247-
wrappedSelector,
248-
equalityFn,
249-
)
346+
// const selectedState = useSyncExternalStoreWithSelector(
347+
// subscription.addNestedSub,
348+
// store.getState,
349+
// getServerState || store.getState,
350+
// wrappedSelector,
351+
// equalityFn,
352+
// )
353+
354+
const selectedState = useStoreSelector(reactStore as any, stableSelector)
250355

251356
React.useDebugValue(selectedState)
252357

0 commit comments

Comments
 (0)