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.
Andrew Clark committed
Aug 28, 2023 at 11:04 UTC
9a01c8b54e91c588391070c65701342e4e361ea7
2 files changed
+59
-2
packages/react-reconciler/src/ReactFiberHooks.js
+21
-2
@@ -1793,7 +1793,20 @@ function updateOptimistic<S, A>(
1793
reducer: ?(S, A) => S,
1794
): [S, (A) => void] {
1795
const hook = updateWorkInProgressHook();
1796
+ return updateOptimisticImpl(
1797
+ hook,
1798
+ ((currentHook: any): Hook),
1799
+ passthrough,
1800
+ reducer,
1801
+ );
1802
+}
1803
1804
+function updateOptimisticImpl<S, A>(
1805
+ hook: Hook,
1806
+ current: Hook | null,
1807
+ passthrough: S,
1808
+ reducer: ?(S, A) => S,
1809
+): [S, (A) => void] {
1810
// Optimistic updates are always rebased on top of the latest value passed in
1811
// as an argument. It's called a passthrough because if there are no pending
1812
// updates, it will be returned as-is.
@@ -1820,14 +1833,20 @@ function rerenderOptimistic<S, A>(
1833
// So instead of a forked re-render implementation that knows how to handle
1834
// render phase udpates, we can use the same implementation as during a
1835
// regular mount or update.
1836
+ const hook = updateWorkInProgressHook();
1837
1838
if (currentHook !== null) {
1839
// 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
+ );
1846
}
1847
1848
// This is a mount. No updates to process.
1830
- const hook = updateWorkInProgressHook();
1849
+
1850
// Reset the base state and memoized state to the passthrough. Future
1851
// updates will be applied on top of this.
1852
hook.baseState = hook.memoizedState = passthrough;
packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+38
@@ -818,6 +818,44 @@ describe('ReactAsyncActions', () => {
818
);
819
});
820
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
+
859
// @gate enableAsyncActions
860
test('useOptimistic accepts a custom reducer', async () => {
861
let serverCart = ['A'];