|
9 | 9 | useReduxContext as useDefaultReduxContext, |
10 | 10 | } from './useReduxContext' |
11 | 11 |
|
| 12 | +import { experimental } from 'react-concurrent-store' |
| 13 | +const { useStoreSelector } = experimental |
| 14 | + |
12 | 15 | /** |
13 | 16 | * The frequency of development mode checks. |
14 | 17 | * |
@@ -161,10 +164,109 @@ export function createSelectorHook( |
161 | 164 |
|
162 | 165 | const reduxContext = useReduxContext() |
163 | 166 |
|
164 | | - const { store, subscription, getServerState } = reduxContext |
| 167 | + const { store, reactStore, subscription, getServerState } = reduxContext |
165 | 168 |
|
| 169 | + const selectorRef = React.useRef(selector) |
| 170 | + const equalityFnRef = React.useRef(equalityFn) |
| 171 | + const lastResultRef = React.useRef<Selected>(null) |
166 | 172 | const firstRun = React.useRef(true) |
167 | 173 |
|
| 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 | + /* |
168 | 270 | const wrappedSelector = React.useCallback<typeof selector>( |
169 | 271 | { |
170 | 272 | [selector.name](state: TState) { |
@@ -239,14 +341,17 @@ export function createSelectorHook( |
239 | 341 | }[selector.name], |
240 | 342 | [selector], |
241 | 343 | ) |
| 344 | + */ |
242 | 345 |
|
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) |
250 | 355 |
|
251 | 356 | React.useDebugValue(selectedState) |
252 | 357 |
|
|
0 commit comments