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.
This commit is contained in:
Andrew Clark
2024-01-13 21:37:35 -05:00
committed by GitHub
parent 33068c9db9
commit 60a927d04a
2 changed files with 57 additions and 2 deletions
+11 -2
View File
@@ -1256,10 +1256,19 @@ function updateReducerImpl<S, A>(
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;
@@ -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 (
<>
<div>
<Text text={'Canonical: ' + canonicalState} />
</div>
<div>
<Text text={'Optimistic: ' + optimisticState} />
</div>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog(['Canonical: 0', 'Optimistic: 0']);
expect(root).toMatchRenderedOutput(
<>
<div>Canonical: 0</div>
<div>Optimistic: 0</div>
</>,
);
// 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(
<>
<div>Canonical: 1</div>
<div>Optimistic: 1</div>
</>,
);
},
);
// @gate enableAsyncActions
test('regression: useOptimistic during setState-in-render', async () => {
// This is a regression test for a very specific case where useOptimistic is