Skip to content

Commit 60a927d

Browse files
authored
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.
1 parent 33068c9 commit 60a927d

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,10 +1256,19 @@ function updateReducerImpl<S, A>(
12561256
queue.pending = null;
12571257
}
12581258

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 {
12601269
// We have a queue to process.
12611270
const first = baseQueue.next;
1262-
let newState = hook.baseState;
1271+
let newState = baseState;
12631272

12641273
let newBaseState = null;
12651274
let newBaseQueueFirst = null;

packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,52 @@ describe('ReactAsyncActions', () => {
818818
);
819819
});
820820

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+
821867
// @gate enableAsyncActions
822868
test('regression: useOptimistic during setState-in-render', async () => {
823869
// This is a regression test for a very specific case where useOptimistic is

0 commit comments

Comments
 (0)