diff --git a/src/alternate-renderers.js b/src/alternate-renderers.js index c3a04a254..0625e8cb2 100644 --- a/src/alternate-renderers.js +++ b/src/alternate-renderers.js @@ -8,6 +8,7 @@ import { useSelector } from './hooks/useSelector' import { useStore } from './hooks/useStore' import { getBatch } from './utils/batch' +import shallowEqual from './utils/shallowEqual' // For other renderers besides ReactDOM and React Native, use the default noop batch function const batch = getBatch() @@ -20,5 +21,6 @@ export { batch, useDispatch, useSelector, - useStore + useStore, + shallowEqual } diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index 8f331f9cd..83e48569d 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -52,12 +52,20 @@ export function useSelector(selector, equalityFn = refEquality) { ]) const latestSubscriptionCallbackError = useRef() - const latestSelector = useRef(selector) + const latestSelector = useRef() + const latestSelectedState = useRef() - let selectedState = undefined + let selectedState try { - selectedState = selector(store.getState()) + if ( + selector !== latestSelector.current || + latestSubscriptionCallbackError.current + ) { + selectedState = selector(store.getState()) + } else { + selectedState = latestSelectedState.current + } } catch (err) { let errorMessage = `An error occured while selecting the store state: ${ err.message @@ -72,8 +80,6 @@ export function useSelector(selector, equalityFn = refEquality) { throw new Error(errorMessage) } - const latestSelectedState = useRef(selectedState) - useIsomorphicLayoutEffect(() => { latestSelector.current = selector latestSelectedState.current = selectedState diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index 8f9409234..a4756d244 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -1,6 +1,6 @@ /*eslint-disable react/prop-types*/ -import React from 'react' +import React, { useCallback, useReducer } from 'react' import { createStore } from 'redux' import { renderHook, act } from 'react-hooks-testing-library' import * as rtl from 'react-testing-library' @@ -51,6 +51,29 @@ describe('React', () => { }) describe('lifeycle interactions', () => { + it('always uses the latest state', () => { + store = createStore(c => c + 1, -1) + + const Comp = () => { + const selector = useCallback(c => c + 1, []) + const value = useSelector(selector) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(renderedItems).toEqual([1]) + + store.dispatch({ type: '' }) + + expect(renderedItems).toEqual([1, 2]) + }) + it('subscribes to the store synchronously', () => { let rootSubscription @@ -183,6 +206,39 @@ describe('React', () => { }) }) + it('uses the latest selector', () => { + let selectorId = 0 + let forceRender + + const Comp = () => { + const [, f] = useReducer(c => c + 1, 0) + forceRender = f + const renderedSelectorId = selectorId++ + const value = useSelector(() => renderedSelectorId) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(renderedItems).toEqual([0]) + + rtl.act(forceRender) + expect(renderedItems).toEqual([0, 1]) + + rtl.act(() => { + store.dispatch({ type: '' }) + }) + expect(renderedItems).toEqual([0, 1]) + + rtl.act(forceRender) + expect(renderedItems).toEqual([0, 1, 2]) + }) + describe('edge cases', () => { it('ignores transient errors in selector (e.g. due to stale props)', () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => {})