@samitouri / QOS-React-1 / commits / 60a927d04a

Fix: useOptimistic should return passthrough value when there are no updates pending (#27936)

This fixes a bug that happened when the canonical value passed to useOptimistic without an accompanying call to setOptimistic. In this scenario, useOptimistic should pass through the new canonical value. I had written tests for the more complicated scenario, where a new value is passed while there are still pending optimistic updates, but not this simpler one.

Andrew Clark committed Jan 13, 2024 at 21:37 UTC 60a927d04ad3888facebcdf7da620aa1cfc9528f
2 files changed +57 -2
packages/react-reconciler/src/ReactFiberHooks.js
+11 -2
@@ -1256,10 +1256,19 @@ function updateReducerImpl<S, A>(
1256 queue.pending = null;
1257 }
1258
1259 - if (baseQueue !== null) {
1259 + const baseState = hook.baseState;
1260 + if (baseQueue === null) {
1261 + // If there are no pending updates, then the memoized state should be the
1262 + // same as the base state. Currently these only diverge in the case of
1263 + // useOptimistic, because useOptimistic accepts a new baseState on
1264 + // every render.
1265 + hook.memoizedState = baseState;
1266 + // We don't need to call markWorkInProgressReceivedUpdate because
1267 + // baseState is derived from other reactive values.
1268 + } else {
1269 // We have a queue to process.
1270 const first = baseQueue.next;
1262 - let newState = hook.baseState;
1271 + let newState = baseState;
1272
1273 let newBaseState = null;
1274 let newBaseQueueFirst = null;
packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+46
@@ -818,6 +818,52 @@ describe('ReactAsyncActions', () => {
818 );
819 });
820
821 + // @gate enableAsyncActions
822 + test(
823 + 'regression: when there are no pending transitions, useOptimistic should ' +
824 + 'always return the passthrough value',
825 + async () => {
826 + let setCanonicalState;
827 + function App() {
828 + const [canonicalState, _setCanonicalState] = useState(0);
829 + const [optimisticState] = useOptimistic(canonicalState);
830 + setCanonicalState = _setCanonicalState;
831 +
832 + return (
833 + <>
834 + <div>
835 + <Text text={'Canonical: ' + canonicalState} />
836 + </div>
837 + <div>
838 + <Text text={'Optimistic: ' + optimisticState} />
839 + </div>
840 + </>
841 + );
842 + }
843 +
844 + const root = ReactNoop.createRoot();
845 + await act(() => root.render(<App />));
846 + assertLog(['Canonical: 0', 'Optimistic: 0']);
847 + expect(root).toMatchRenderedOutput(
848 + <>
849 + <div>Canonical: 0</div>
850 + <div>Optimistic: 0</div>
851 + </>,
852 + );
853 +
854 + // Update the canonical state. The optimistic state should update, too,
855 + // even though there was no transition, and no call to setOptimisticState.
856 + await act(() => setCanonicalState(1));
857 + assertLog(['Canonical: 1', 'Optimistic: 1']);
858 + expect(root).toMatchRenderedOutput(
859 + <>
860 + <div>Canonical: 1</div>
861 + <div>Optimistic: 1</div>
862 + </>,
863 + );
864 + },
865 + );
866 +
867 // @gate enableAsyncActions
868 test('regression: useOptimistic during setState-in-render', async () => {
869 // This is a regression test for a very specific case where useOptimistic is