Skip to content

Commit 9a01c8b

Browse files
authored
Fix mount-or-update check in rerenderOptimistic (#27277)
I noticed this was wrong because it should call updateWorkInProgressHook before it checks if currentHook is null.
1 parent b798223 commit 9a01c8b

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,7 +1793,20 @@ function updateOptimistic<S, A>(
17931793
reducer: ?(S, A) => S,
17941794
): [S, (A) => void] {
17951795
const hook = updateWorkInProgressHook();
1796+
return updateOptimisticImpl(
1797+
hook,
1798+
((currentHook: any): Hook),
1799+
passthrough,
1800+
reducer,
1801+
);
1802+
}
17961803

1804+
function updateOptimisticImpl<S, A>(
1805+
hook: Hook,
1806+
current: Hook | null,
1807+
passthrough: S,
1808+
reducer: ?(S, A) => S,
1809+
): [S, (A) => void] {
17971810
// Optimistic updates are always rebased on top of the latest value passed in
17981811
// as an argument. It's called a passthrough because if there are no pending
17991812
// updates, it will be returned as-is.
@@ -1820,14 +1833,20 @@ function rerenderOptimistic<S, A>(
18201833
// So instead of a forked re-render implementation that knows how to handle
18211834
// render phase udpates, we can use the same implementation as during a
18221835
// regular mount or update.
1836+
const hook = updateWorkInProgressHook();
18231837

18241838
if (currentHook !== null) {
18251839
// This is an update. Process the update queue.
1826-
return updateOptimistic(passthrough, reducer);
1840+
return updateOptimisticImpl(
1841+
hook,
1842+
((currentHook: any): Hook),
1843+
passthrough,
1844+
reducer,
1845+
);
18271846
}
18281847

18291848
// This is a mount. No updates to process.
1830-
const hook = updateWorkInProgressHook();
1849+
18311850
// Reset the base state and memoized state to the passthrough. Future
18321851
// updates will be applied on top of this.
18331852
hook.baseState = hook.memoizedState = passthrough;

packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,44 @@ describe('ReactAsyncActions', () => {
818818
);
819819
});
820820

821+
// @gate enableAsyncActions
822+
test('regression: useOptimistic during setState-in-render', async () => {
823+
// This is a regression test for a very specific case where useOptimistic is
824+
// the first hook in the component, it has a pending update, and a later
825+
// hook schedules a local (setState-in-render) update. Don't sweat about
826+
// deleting this test if the implementation details change.
827+
828+
let setOptimisticState;
829+
let startTransition;
830+
function App() {
831+
const [optimisticState, _setOptimisticState] = useOptimistic(0);
832+
setOptimisticState = _setOptimisticState;
833+
const [, _startTransition] = useTransition();
834+
startTransition = _startTransition;
835+
836+
const [derivedState, setDerivedState] = useState(0);
837+
if (derivedState !== optimisticState) {
838+
setDerivedState(optimisticState);
839+
}
840+
841+
return <Text text={optimisticState} />;
842+
}
843+
844+
const root = ReactNoop.createRoot();
845+
await act(() => root.render(<App />));
846+
assertLog([0]);
847+
expect(root).toMatchRenderedOutput('0');
848+
849+
await act(() => {
850+
startTransition(async () => {
851+
setOptimisticState(1);
852+
await getText('Wait');
853+
});
854+
});
855+
assertLog([1]);
856+
expect(root).toMatchRenderedOutput('1');
857+
});
858+
821859
// @gate enableAsyncActions
822860
test('useOptimistic accepts a custom reducer', async () => {
823861
let serverCart = ['A'];

0 commit comments

Comments
 (0)