From c506ded3b275b410517c06e0f55db8f0f70288b5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 19 Feb 2019 18:40:10 +0000 Subject: [PATCH] Don't discard render phase state updates with the eager reducer optimization (#14852) * Add test cases for setState(fn) + render phase updates * Update eager state and reducer for render phase updates * Fix a newly firing warning --- .../react-reconciler/src/ReactFiberHooks.js | 4 +- .../src/__tests__/ReactHooks-test.internal.js | 70 +++++++++++++++++++ ...eactHooksWithNoopRenderer-test.internal.js | 4 +- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 0b5f0172e7ea1..830e87ccc6612 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -607,7 +607,6 @@ function updateReducer( } hook.memoizedState = newState; - // Don't persist the state accumlated from the render phase updates to // the base state unless the queue is empty. // TODO: Not sure if this is the desired semantics, but it's what we @@ -616,6 +615,9 @@ function updateReducer( hook.baseState = newState; } + queue.eagerReducer = reducer; + queue.eagerState = newState; + return [newState, dispatch]; } } diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index f7ab26011b64e..2e11f01d5a2d6 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -669,6 +669,76 @@ describe('ReactHooks', () => { }).toThrow('is not a function'); }); + it('does not forget render phase useState updates inside an effect', () => { + const {useState, useEffect} = React; + + function Counter() { + const [counter, setCounter] = useState(0); + if (counter === 0) { + setCounter(x => x + 1); + setCounter(x => x + 1); + } + useEffect(() => { + setCounter(x => x + 1); + setCounter(x => x + 1); + }, []); + return counter; + } + + const root = ReactTestRenderer.create(null); + ReactTestRenderer.act(() => { + root.update(); + }); + expect(root).toMatchRenderedOutput('4'); + }); + + it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => { + const {useReducer, useEffect} = React; + + const reducer = x => x + 1; + function Counter() { + const [counter, increment] = useReducer(reducer, 0); + if (counter === 0) { + increment(); + increment(); + } + useEffect(() => { + increment(); + increment(); + }, []); + return counter; + } + + const root = ReactTestRenderer.create(null); + ReactTestRenderer.act(() => { + root.update(); + }); + expect(root).toMatchRenderedOutput('4'); + }); + + it('does not forget render phase useReducer updates inside an effect with inline reducer', () => { + const {useReducer, useEffect} = React; + + function Counter() { + const [counter, increment] = useReducer(x => x + 1, 0); + if (counter === 0) { + increment(); + increment(); + } + useEffect(() => { + increment(); + increment(); + }, []); + return counter; + } + + const root = ReactTestRenderer.create(null); + ReactTestRenderer.act(() => { + root.update(); + }); + expect(root).toMatchRenderedOutput('4'); + }); + it('warns for bad useImperativeHandle first arg', () => { const {useImperativeHandle} = React; function App() { diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 290651b117e77..2995a3349759c 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -454,7 +454,9 @@ describe('ReactHooksWithNoopRenderer', () => { // Test that it works on update, too. This time the log is a bit different // because we started with reducerB instead of reducerA. - counter.current.dispatch('reset'); + ReactNoop.act(() => { + counter.current.dispatch('reset'); + }); ReactNoop.render(); expect(ReactNoop.flush()).toEqual([ 'Render: 0',