@samitouri / QOS-React-2 / commits / 9a01c8b54e

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'];