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
Dan Abramov committed
Feb 19, 2019 at 18:40 UTC
c506ded3b275b410517c06e0f55db8f0f70288b5
3 files changed
+76
-2
packages/react-reconciler/src/ReactFiberHooks.js
+3
-1
@@ -607,7 +607,6 @@ function updateReducer<S, I, A>(
607
}
608
609
hook.memoizedState = newState;
610
-
610
// Don't persist the state accumlated from the render phase updates to
611
// the base state unless the queue is empty.
612
// TODO: Not sure if this is the desired semantics, but it's what we
@@ -616,6 +615,9 @@ function updateReducer<S, I, A>(
615
hook.baseState = newState;
616
}
617
618
+ queue.eagerReducer = reducer;
619
+ queue.eagerState = newState;
620
+
621
return [newState, dispatch];
622
}
623
}
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+70
@@ -669,6 +669,76 @@ describe('ReactHooks', () => {
669
}).toThrow('is not a function');
670
});
671
672
+ it('does not forget render phase useState updates inside an effect', () => {
673
+ const {useState, useEffect} = React;
674
+
675
+ function Counter() {
676
+ const [counter, setCounter] = useState(0);
677
+ if (counter === 0) {
678
+ setCounter(x => x + 1);
679
+ setCounter(x => x + 1);
680
+ }
681
+ useEffect(() => {
682
+ setCounter(x => x + 1);
683
+ setCounter(x => x + 1);
684
+ }, []);
685
+ return counter;
686
+ }
687
+
688
+ const root = ReactTestRenderer.create(null);
689
+ ReactTestRenderer.act(() => {
690
+ root.update(<Counter />);
691
+ });
692
+ expect(root).toMatchRenderedOutput('4');
693
+ });
694
+
695
+ it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
696
+ const {useReducer, useEffect} = React;
697
+
698
+ const reducer = x => x + 1;
699
+ function Counter() {
700
+ const [counter, increment] = useReducer(reducer, 0);
701
+ if (counter === 0) {
702
+ increment();
703
+ increment();
704
+ }
705
+ useEffect(() => {
706
+ increment();
707
+ increment();
708
+ }, []);
709
+ return counter;
710
+ }
711
+
712
+ const root = ReactTestRenderer.create(null);
713
+ ReactTestRenderer.act(() => {
714
+ root.update(<Counter />);
715
+ });
716
+ expect(root).toMatchRenderedOutput('4');
717
+ });
718
+
719
+ it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
720
+ const {useReducer, useEffect} = React;
721
+
722
+ function Counter() {
723
+ const [counter, increment] = useReducer(x => x + 1, 0);
724
+ if (counter === 0) {
725
+ increment();
726
+ increment();
727
+ }
728
+ useEffect(() => {
729
+ increment();
730
+ increment();
731
+ }, []);
732
+ return counter;
733
+ }
734
+
735
+ const root = ReactTestRenderer.create(null);
736
+ ReactTestRenderer.act(() => {
737
+ root.update(<Counter />);
738
+ });
739
+ expect(root).toMatchRenderedOutput('4');
740
+ });
741
+
742
it('warns for bad useImperativeHandle first arg', () => {
743
const {useImperativeHandle} = React;
744
function App() {
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+3
-1
@@ -454,7 +454,9 @@ describe('ReactHooksWithNoopRenderer', () => {
454
455
// Test that it works on update, too. This time the log is a bit different
456
// because we started with reducerB instead of reducerA.
457
- counter.current.dispatch('reset');
457
+ ReactNoop.act(() => {
458
+ counter.current.dispatch('reset');
459
+ });
460
ReactNoop.render(<Counter ref={counter} />);
461
expect(ReactNoop.flush()).toEqual([
462
'Render: 0',