Skip to content

Commit 2a5b12b

Browse files
dai-shitimdorr
andauthored
fix useSelector race condition with memoized selector when dispatching in child components useLayoutEffect as well as cDM/cDU (#1536)
Co-authored-by: Tim Dorr <[email protected]>
1 parent 79f3d13 commit 2a5b12b

File tree

2 files changed

+42
-2
lines changed

2 files changed

+42
-2
lines changed

src/hooks/useSelector.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ function useSelectorWithStoreAndSubscription(
2121

2222
const latestSubscriptionCallbackError = useRef()
2323
const latestSelector = useRef()
24+
const latestStoreState = useRef()
2425
const latestSelectedState = useRef()
2526

27+
const storeState = store.getState()
2628
let selectedState
2729

2830
try {
2931
if (
3032
selector !== latestSelector.current ||
33+
storeState !== latestStoreState.current ||
3134
latestSubscriptionCallbackError.current
3235
) {
33-
selectedState = selector(store.getState())
36+
selectedState = selector(storeState)
3437
} else {
3538
selectedState = latestSelectedState.current
3639
}
@@ -44,6 +47,7 @@ function useSelectorWithStoreAndSubscription(
4447

4548
useIsomorphicLayoutEffect(() => {
4649
latestSelector.current = selector
50+
latestStoreState.current = storeState
4751
latestSelectedState.current = selectedState
4852
latestSubscriptionCallbackError.current = undefined
4953
})

test/hooks/useSelector.spec.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*eslint-disable react/prop-types*/
22

3-
import React, { useCallback, useReducer } from 'react'
3+
import React, { useCallback, useReducer, useLayoutEffect } from 'react'
44
import { createStore } from 'redux'
55
import { renderHook, act } from '@testing-library/react-hooks'
66
import * as rtl from '@testing-library/react'
@@ -156,6 +156,42 @@ describe('React', () => {
156156
})
157157
})
158158

159+
it('works properly with memoized selector with dispatch in Child useLayoutEffect', () => {
160+
store = createStore(c => c + 1, -1)
161+
162+
const Comp = () => {
163+
const selector = useCallback(c => c, [])
164+
const count = useSelector(selector)
165+
renderedItems.push(count)
166+
return <Child parentCount={count} />
167+
}
168+
169+
const Child = ({ parentCount }) => {
170+
useLayoutEffect(() => {
171+
if (parentCount === 1) {
172+
store.dispatch({ type: '' })
173+
}
174+
}, [parentCount])
175+
return <div>{parentCount}</div>
176+
}
177+
178+
rtl.render(
179+
<ProviderMock store={store}>
180+
<Comp />
181+
</ProviderMock>
182+
)
183+
184+
// The first render doesn't trigger dispatch
185+
expect(renderedItems).toEqual([0])
186+
187+
// This dispatch triggers another dispatch in useLayoutEffect
188+
rtl.act(() => {
189+
store.dispatch({ type: '' })
190+
})
191+
192+
expect(renderedItems).toEqual([0, 1, 2])
193+
})
194+
159195
describe('performance optimizations and bail-outs', () => {
160196
it('defaults to ref-equality to prevent unnecessary updates', () => {
161197
const state = {}

0 commit comments

Comments
 (0)