diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index 85e85d13e..652bcef33 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -65,6 +65,11 @@ function useSelectorWithStoreAndSubscription( function checkForUpdates() { try { const newStoreState = store.getState() + // Avoid calling selector multiple times if the store's state has not changed + if (newStoreState === latestStoreState.current) { + return + } + const newSelectedState = latestSelector.current(newStoreState) if (equalityFn(newSelectedState, latestSelectedState.current)) { diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index 17d7a80aa..8eb4bddf4 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -45,14 +45,14 @@ describe('React', () => { }) expect(result.current).toEqual(0) - expect(selector).toHaveBeenCalledTimes(2) + expect(selector).toHaveBeenCalledTimes(1) act(() => { store.dispatch({ type: '' }) }) expect(result.current).toEqual(1) - expect(selector).toHaveBeenCalledTimes(3) + expect(selector).toHaveBeenCalledTimes(2) }) }) @@ -246,6 +246,79 @@ describe('React', () => { expect(renderedItems.length).toBe(1) }) + + it('calls selector exactly once on mount and on update', () => { + store = createStore(({ count } = { count: 0 }) => ({ + count: count + 1, + })) + + let numCalls = 0 + const selector = (s) => { + numCalls += 1 + return s.count + } + const renderedItems = [] + + const Comp = () => { + const value = useSelector(selector) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(numCalls).toBe(1) + expect(renderedItems.length).toEqual(1) + + store.dispatch({ type: '' }) + + expect(numCalls).toBe(2) + expect(renderedItems.length).toEqual(2) + }) + + it('calls selector twice once on mount when state changes during render', () => { + store = createStore(({ count } = { count: 0 }) => ({ + count: count + 1, + })) + + let numCalls = 0 + const selector = (s) => { + numCalls += 1 + return s.count + } + const renderedItems = [] + + const Child = () => { + useLayoutEffect(() => { + store.dispatch({ type: '', count: 1 }) + }, []) + return
+ } + + const Comp = () => { + const value = useSelector(selector) + renderedItems.push(value) + return ( +
+ +
+ ) + } + + rtl.render( + + + + ) + + // Selector first called on Comp mount, and then re-invoked after mount due to useLayoutEffect dispatching event + expect(numCalls).toBe(2) + expect(renderedItems.length).toEqual(2) + }) }) it('uses the latest selector', () => {