From 58a964cabe022f41ddb873f6f2c3520ed18d1a1b Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 5 Mar 2020 18:56:31 +0900 Subject: [PATCH] fix useSelector race condition with memoized selector when dispatching in child components useLayoutEffect as well as cDM/cDU --- src/hooks/useSelector.js | 6 +++++- test/hooks/useSelector.spec.js | 38 +++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index d6e85928a..ceb429546 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -21,16 +21,19 @@ function useSelectorWithStoreAndSubscription( const latestSubscriptionCallbackError = useRef() const latestSelector = useRef() + const latestStoreState = useRef() const latestSelectedState = useRef() + const storeState = store.getState() let selectedState try { if ( selector !== latestSelector.current || + storeState !== latestStoreState.current || latestSubscriptionCallbackError.current ) { - selectedState = selector(store.getState()) + selectedState = selector(storeState) } else { selectedState = latestSelectedState.current } @@ -44,6 +47,7 @@ function useSelectorWithStoreAndSubscription( useIsomorphicLayoutEffect(() => { latestSelector.current = selector + latestStoreState.current = storeState latestSelectedState.current = selectedState latestSubscriptionCallbackError.current = undefined }) diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index b0c8692b0..269a9f2a6 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -1,6 +1,6 @@ /*eslint-disable react/prop-types*/ -import React, { useCallback, useReducer } from 'react' +import React, { useCallback, useReducer, useLayoutEffect } from 'react' import { createStore } from 'redux' import { renderHook, act } from '@testing-library/react-hooks' import * as rtl from '@testing-library/react' @@ -156,6 +156,42 @@ describe('React', () => { }) }) + it('works properly with memoized selector with dispatch in Child useLayoutEffect', () => { + store = createStore(c => c + 1, -1) + + const Comp = () => { + const selector = useCallback(c => c, []) + const count = useSelector(selector) + renderedItems.push(count) + return + } + + const Child = ({ parentCount }) => { + useLayoutEffect(() => { + if (parentCount === 1) { + store.dispatch({ type: '' }) + } + }, [parentCount]) + return
{parentCount}
+ } + + rtl.render( + + + + ) + + // The first render doesn't trigger dispatch + expect(renderedItems).toEqual([0]) + + // This dispatch triggers another dispatch in useLayoutEffect + rtl.act(() => { + store.dispatch({ type: '' }) + }) + + expect(renderedItems).toEqual([0, 1, 2]) + }) + describe('performance optimizations and bail-outs', () => { it('defaults to ref-equality to prevent unnecessary updates', () => { const state = {}