Skip to content

Commit d9fa20d

Browse files
acdlitegaearon
authored andcommitted
Eager bailout optimization should always compare to latest reducer (facebook#15124)
* Eager bailout optimization should always compare to latest reducer * queue.eagerReducer -> queue.lastRenderedReducer This name is a bit more descriptive. * Add test case that uses preceding render phase update
1 parent 84cc8a3 commit d9fa20d

File tree

2 files changed

+99
-15
lines changed

2 files changed

+99
-15
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ type Update<S, A> = {
8989
type UpdateQueue<S, A> = {
9090
last: Update<S, A> | null,
9191
dispatch: (A => mixed) | null,
92-
eagerReducer: ((S, A) => S) | null,
93-
eagerState: S | null,
92+
lastRenderedReducer: ((S, A) => S) | null,
93+
lastRenderedState: S | null,
9494
};
9595

9696
export type HookType =
@@ -591,8 +591,8 @@ function mountReducer<S, I, A>(
591591
const queue = (hook.queue = {
592592
last: null,
593593
dispatch: null,
594-
eagerReducer: reducer,
595-
eagerState: (initialState: any),
594+
lastRenderedReducer: reducer,
595+
lastRenderedState: (initialState: any),
596596
});
597597
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
598598
null,
@@ -615,6 +615,8 @@ function updateReducer<S, I, A>(
615615
'Should have a queue. This is likely a bug in React. Please file an issue.',
616616
);
617617

618+
queue.lastRenderedReducer = reducer;
619+
618620
if (numberOfReRenders > 0) {
619621
// This is a re-render. Apply the new render phase updates to the previous
620622
// work-in-progress hook.
@@ -650,8 +652,7 @@ function updateReducer<S, I, A>(
650652
hook.baseState = newState;
651653
}
652654

653-
queue.eagerReducer = reducer;
654-
queue.eagerState = newState;
655+
queue.lastRenderedState = newState;
655656

656657
return [newState, dispatch];
657658
}
@@ -730,8 +731,7 @@ function updateReducer<S, I, A>(
730731
hook.baseUpdate = newBaseUpdate;
731732
hook.baseState = newBaseState;
732733

733-
queue.eagerReducer = reducer;
734-
queue.eagerState = newState;
734+
queue.lastRenderedState = newState;
735735
}
736736

737737
const dispatch: Dispatch<A> = (queue.dispatch: any);
@@ -749,8 +749,8 @@ function mountState<S>(
749749
const queue = (hook.queue = {
750750
last: null,
751751
dispatch: null,
752-
eagerReducer: basicStateReducer,
753-
eagerState: (initialState: any),
752+
lastRenderedReducer: basicStateReducer,
753+
lastRenderedState: (initialState: any),
754754
});
755755
const dispatch: Dispatch<
756756
BasicStateAction<S>,
@@ -1129,21 +1129,21 @@ function dispatchAction<S, A>(
11291129
// The queue is currently empty, which means we can eagerly compute the
11301130
// next state before entering the render phase. If the new state is the
11311131
// same as the current state, we may be able to bail out entirely.
1132-
const eagerReducer = queue.eagerReducer;
1133-
if (eagerReducer !== null) {
1132+
const lastRenderedReducer = queue.lastRenderedReducer;
1133+
if (lastRenderedReducer !== null) {
11341134
let prevDispatcher;
11351135
if (__DEV__) {
11361136
prevDispatcher = ReactCurrentDispatcher.current;
11371137
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
11381138
}
11391139
try {
1140-
const currentState: S = (queue.eagerState: any);
1141-
const eagerState = eagerReducer(currentState, action);
1140+
const currentState: S = (queue.lastRenderedState: any);
1141+
const eagerState = lastRenderedReducer(currentState, action);
11421142
// Stash the eagerly computed state, and the reducer used to compute
11431143
// it, on the update object. If the reducer hasn't changed by the
11441144
// time we enter the render phase, then the eager state can be used
11451145
// without calling the reducer again.
1146-
update.eagerReducer = eagerReducer;
1146+
update.eagerReducer = lastRenderedReducer;
11471147
update.eagerState = eagerState;
11481148
if (is(eagerState, currentState)) {
11491149
// Fast path. We can bail out without scheduling React to re-render.

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,4 +1871,88 @@ describe('ReactHooksWithNoopRenderer', () => {
18711871
// );
18721872
});
18731873
});
1874+
1875+
it('eager bailout optimization should always compare to latest rendered reducer', () => {
1876+
// Edge case based on a bug report
1877+
let setCounter;
1878+
function App() {
1879+
const [counter, _setCounter] = useState(1);
1880+
setCounter = _setCounter;
1881+
return <Component count={counter} />;
1882+
}
1883+
1884+
function Component({count}) {
1885+
const [state, dispatch] = useReducer(() => {
1886+
// This reducer closes over a value from props. If the reducer is not
1887+
// properly updated, the eager reducer will compare to an old value
1888+
// and bail out incorrectly.
1889+
Scheduler.yieldValue('Reducer: ' + count);
1890+
return count;
1891+
}, -1);
1892+
useEffect(
1893+
() => {
1894+
Scheduler.yieldValue('Effect: ' + count);
1895+
dispatch();
1896+
},
1897+
[count],
1898+
);
1899+
Scheduler.yieldValue('Render: ' + state);
1900+
return count;
1901+
}
1902+
1903+
ReactNoop.render(<App />);
1904+
expect(Scheduler).toFlushAndYield([
1905+
'Render: -1',
1906+
'Effect: 1',
1907+
'Reducer: 1',
1908+
'Reducer: 1',
1909+
'Render: 1',
1910+
]);
1911+
expect(ReactNoop).toMatchRenderedOutput('1');
1912+
1913+
act(() => {
1914+
setCounter(2);
1915+
});
1916+
expect(Scheduler).toFlushAndYield([
1917+
'Render: 1',
1918+
'Effect: 2',
1919+
'Reducer: 2',
1920+
'Reducer: 2',
1921+
'Render: 2',
1922+
]);
1923+
expect(ReactNoop).toMatchRenderedOutput('2');
1924+
});
1925+
1926+
it('should update latest rendered reducer when a preceding state receives a render phase update', () => {
1927+
// Similar to previous test, except using a preceding render phase update
1928+
// instead of new props.
1929+
let dispatch;
1930+
function App() {
1931+
const [step, setStep] = useState(0);
1932+
const [shadow, _dispatch] = useReducer(() => step, step);
1933+
dispatch = _dispatch;
1934+
1935+
if (step < 5) {
1936+
setStep(step + 1);
1937+
}
1938+
1939+
Scheduler.yieldValue(`Step: ${step}, Shadow: ${shadow}`);
1940+
return shadow;
1941+
}
1942+
1943+
ReactNoop.render(<App />);
1944+
expect(Scheduler).toFlushAndYield([
1945+
'Step: 0, Shadow: 0',
1946+
'Step: 1, Shadow: 0',
1947+
'Step: 2, Shadow: 0',
1948+
'Step: 3, Shadow: 0',
1949+
'Step: 4, Shadow: 0',
1950+
'Step: 5, Shadow: 0',
1951+
]);
1952+
expect(ReactNoop).toMatchRenderedOutput('0');
1953+
1954+
act(() => dispatch());
1955+
expect(Scheduler).toFlushAndYield(['Step: 5, Shadow: 5']);
1956+
expect(ReactNoop).toMatchRenderedOutput('5');
1957+
});
18741958
});

0 commit comments

Comments
 (0)