Skip to content

Commit 099e104

Browse files
authored
Reduce unnecessary calls to useSelector selector (#1803)
1 parent e7807ef commit 099e104

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

Diff for: src/hooks/useSelector.js

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ function useSelectorWithStoreAndSubscription(
6565
function checkForUpdates() {
6666
try {
6767
const newStoreState = store.getState()
68+
// Avoid calling selector multiple times if the store's state has not changed
69+
if (newStoreState === latestStoreState.current) {
70+
return
71+
}
72+
6873
const newSelectedState = latestSelector.current(newStoreState)
6974

7075
if (equalityFn(newSelectedState, latestSelectedState.current)) {

Diff for: test/hooks/useSelector.spec.js

+75-2
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ describe('React', () => {
4545
})
4646

4747
expect(result.current).toEqual(0)
48-
expect(selector).toHaveBeenCalledTimes(2)
48+
expect(selector).toHaveBeenCalledTimes(1)
4949

5050
act(() => {
5151
store.dispatch({ type: '' })
5252
})
5353

5454
expect(result.current).toEqual(1)
55-
expect(selector).toHaveBeenCalledTimes(3)
55+
expect(selector).toHaveBeenCalledTimes(2)
5656
})
5757
})
5858

@@ -246,6 +246,79 @@ describe('React', () => {
246246

247247
expect(renderedItems.length).toBe(1)
248248
})
249+
250+
it('calls selector exactly once on mount and on update', () => {
251+
store = createStore(({ count } = { count: 0 }) => ({
252+
count: count + 1,
253+
}))
254+
255+
let numCalls = 0
256+
const selector = (s) => {
257+
numCalls += 1
258+
return s.count
259+
}
260+
const renderedItems = []
261+
262+
const Comp = () => {
263+
const value = useSelector(selector)
264+
renderedItems.push(value)
265+
return <div />
266+
}
267+
268+
rtl.render(
269+
<ProviderMock store={store}>
270+
<Comp />
271+
</ProviderMock>
272+
)
273+
274+
expect(numCalls).toBe(1)
275+
expect(renderedItems.length).toEqual(1)
276+
277+
store.dispatch({ type: '' })
278+
279+
expect(numCalls).toBe(2)
280+
expect(renderedItems.length).toEqual(2)
281+
})
282+
283+
it('calls selector twice once on mount when state changes during render', () => {
284+
store = createStore(({ count } = { count: 0 }) => ({
285+
count: count + 1,
286+
}))
287+
288+
let numCalls = 0
289+
const selector = (s) => {
290+
numCalls += 1
291+
return s.count
292+
}
293+
const renderedItems = []
294+
295+
const Child = () => {
296+
useLayoutEffect(() => {
297+
store.dispatch({ type: '', count: 1 })
298+
}, [])
299+
return <div />
300+
}
301+
302+
const Comp = () => {
303+
const value = useSelector(selector)
304+
renderedItems.push(value)
305+
return (
306+
<div>
307+
<Child />
308+
</div>
309+
)
310+
}
311+
312+
rtl.render(
313+
<ProviderMock store={store}>
314+
<Comp />
315+
</ProviderMock>
316+
)
317+
318+
// Selector first called on Comp mount, and then re-invoked after mount due to useLayoutEffect dispatching event
319+
expect(numCalls).toBe(2)
320+
expect(renderedItems.length).toEqual(2)
321+
})
249322
})
250323

251324
it('uses the latest selector', () => {

0 commit comments

Comments
 (0)