diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index e58c5484ef92..12c398709528 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1256,10 +1256,19 @@ function updateReducerImpl( queue.pending = null; } - if (baseQueue !== null) { + const baseState = hook.baseState; + if (baseQueue === null) { + // If there are no pending updates, then the memoized state should be the + // same as the base state. Currently these only diverge in the case of + // useOptimistic, because useOptimistic accepts a new baseState on + // every render. + hook.memoizedState = baseState; + // We don't need to call markWorkInProgressReceivedUpdate because + // baseState is derived from other reactive values. + } else { // We have a queue to process. const first = baseQueue.next; - let newState = hook.baseState; + let newState = baseState; let newBaseState = null; let newBaseQueueFirst = null; diff --git a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js index 0bf859f18862..1be91d36084e 100644 --- a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js +++ b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js @@ -818,6 +818,52 @@ describe('ReactAsyncActions', () => { ); }); + // @gate enableAsyncActions + test( + 'regression: when there are no pending transitions, useOptimistic should ' + + 'always return the passthrough value', + async () => { + let setCanonicalState; + function App() { + const [canonicalState, _setCanonicalState] = useState(0); + const [optimisticState] = useOptimistic(canonicalState); + setCanonicalState = _setCanonicalState; + + return ( + <> +
+ +
+
+ +
+ + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog(['Canonical: 0', 'Optimistic: 0']); + expect(root).toMatchRenderedOutput( + <> +
Canonical: 0
+
Optimistic: 0
+ , + ); + + // Update the canonical state. The optimistic state should update, too, + // even though there was no transition, and no call to setOptimisticState. + await act(() => setCanonicalState(1)); + assertLog(['Canonical: 1', 'Optimistic: 1']); + expect(root).toMatchRenderedOutput( + <> +
Canonical: 1
+
Optimistic: 1
+ , + ); + }, + ); + // @gate enableAsyncActions test('regression: useOptimistic during setState-in-render', async () => { // This is a regression test for a very specific case where useOptimistic is