Skip to content

Commit a3503c1

Browse files
authored
Reduce unnecessary calls to useSelector selector (#1804)
1 parent 5673e9b commit a3503c1

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

src/hooks/useSelector.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ function useSelectorWithStoreAndSubscription<TStoreState, TSelectedState>(
7171
function checkForUpdates() {
7272
try {
7373
const newStoreState = store.getState()
74+
// Avoid calling selector multiple times if the store's state has not changed
75+
if (newStoreState === latestStoreState.current) {
76+
return
77+
}
78+
7479
const newSelectedState = latestSelector.current!(newStoreState)
7580

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

test/hooks/useSelector.spec.tsx

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ describe('React', () => {
7272
})
7373

7474
expect(result.current).toEqual(0)
75-
expect(selector).toHaveBeenCalledTimes(2)
75+
expect(selector).toHaveBeenCalledTimes(1)
7676

7777
act(() => {
7878
normalStore.dispatch({ type: '' })
7979
})
8080

8181
expect(result.current).toEqual(1)
82-
expect(selector).toHaveBeenCalledTimes(3)
82+
expect(selector).toHaveBeenCalledTimes(2)
8383
})
8484
})
8585

@@ -283,6 +283,85 @@ describe('React', () => {
283283

284284
expect(renderedItems.length).toBe(1)
285285
})
286+
287+
it('calls selector exactly once on mount and on update', () => {
288+
interface StateType {
289+
count: number
290+
}
291+
const store = createStore(({ count }: StateType = { count: 0 }) => ({
292+
count: count + 1,
293+
}))
294+
295+
let numCalls = 0
296+
const selector = (s: StateType) => {
297+
numCalls += 1
298+
return s.count
299+
}
300+
const renderedItems = []
301+
302+
const Comp = () => {
303+
const value = useSelector(selector)
304+
renderedItems.push(value)
305+
return <div />
306+
}
307+
308+
rtl.render(
309+
<ProviderMock store={store}>
310+
<Comp />
311+
</ProviderMock>
312+
)
313+
314+
expect(numCalls).toBe(1)
315+
expect(renderedItems.length).toEqual(1)
316+
317+
store.dispatch({ type: '' })
318+
319+
expect(numCalls).toBe(2)
320+
expect(renderedItems.length).toEqual(2)
321+
})
322+
323+
it('calls selector twice once on mount when state changes during render', () => {
324+
interface StateType {
325+
count: number
326+
}
327+
const store = createStore(({ count }: StateType = { count: 0 }) => ({
328+
count: count + 1,
329+
}))
330+
331+
let numCalls = 0
332+
const selector = (s: StateType) => {
333+
numCalls += 1
334+
return s.count
335+
}
336+
const renderedItems = []
337+
338+
const Child = () => {
339+
useLayoutEffect(() => {
340+
store.dispatch({ type: '', count: 1 })
341+
}, [])
342+
return <div />
343+
}
344+
345+
const Comp = () => {
346+
const value = useSelector(selector)
347+
renderedItems.push(value)
348+
return (
349+
<div>
350+
<Child />
351+
</div>
352+
)
353+
}
354+
355+
rtl.render(
356+
<ProviderMock store={store}>
357+
<Comp />
358+
</ProviderMock>
359+
)
360+
361+
// Selector first called on Comp mount, and then re-invoked after mount due to useLayoutEffect dispatching event
362+
expect(numCalls).toBe(2)
363+
expect(renderedItems.length).toEqual(2)
364+
})
286365
})
287366

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

0 commit comments

Comments
 (0)