Skip to content

Commit 619cdfc

Browse files
gaearonacdlite
authored andcommitted
Don't discard render phase state updates with the eager reducer optimization (facebook#14852)
* Add test cases for setState(fn) + render phase updates * Update eager state and reducer for render phase updates * Fix a newly firing warning
1 parent 3e55560 commit 619cdfc

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,6 @@ function updateReducer<S, I, A>(
607607
}
608608

609609
hook.memoizedState = newState;
610-
611610
// Don't persist the state accumlated from the render phase updates to
612611
// the base state unless the queue is empty.
613612
// TODO: Not sure if this is the desired semantics, but it's what we
@@ -616,6 +615,9 @@ function updateReducer<S, I, A>(
616615
hook.baseState = newState;
617616
}
618617

618+
queue.eagerReducer = reducer;
619+
queue.eagerState = newState;
620+
619621
return [newState, dispatch];
620622
}
621623
}

packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

+70
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,76 @@ describe('ReactHooks', () => {
669669
}).toThrow('is not a function');
670670
});
671671

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+
672742
it('warns for bad useImperativeHandle first arg', () => {
673743
const {useImperativeHandle} = React;
674744
function App() {

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,9 @@ describe('ReactHooksWithNoopRenderer', () => {
454454

455455
// Test that it works on update, too. This time the log is a bit different
456456
// because we started with reducerB instead of reducerA.
457-
counter.current.dispatch('reset');
457+
ReactNoop.act(() => {
458+
counter.current.dispatch('reset');
459+
});
458460
ReactNoop.render(<Counter ref={counter} />);
459461
expect(ReactNoop.flush()).toEqual([
460462
'Render: 0',

0 commit comments

Comments
 (0)