Skip to content

Commit

Permalink
Fix: useOptimistic should return passthrough value when there are no …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
acdlite committed Jan 14, 2024
1 parent 33068c9 commit 60a927d
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 2 deletions.
13 changes: 11 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 60a927d

Please sign in to comment.