diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js index 0bdcbf580764a..06ee95a92c705 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -580,7 +580,12 @@ export function processUpdateQueue( instance, ); const callback = update.callback; - if (callback !== null) { + if ( + callback !== null && + // If the update was already committed, we should not queue its + // callback again. + update.lane !== NoLane + ) { workInProgress.flags |= Callback; const effects = queue.effects; if (effects === null) { diff --git a/packages/react-reconciler/src/ReactUpdateQueue.old.js b/packages/react-reconciler/src/ReactUpdateQueue.old.js index 209abfb32e743..520f993fbd81f 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.old.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.old.js @@ -580,7 +580,12 @@ export function processUpdateQueue( instance, ); const callback = update.callback; - if (callback !== null) { + if ( + callback !== null && + // If the update was already committed, we should not queue its + // callback again. + update.lane !== NoLane + ) { workInProgress.flags |= Callback; const effects = queue.effects; if (effects === null) { diff --git a/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js new file mode 100644 index 0000000000000..13d9ce838c03b --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js @@ -0,0 +1,47 @@ +let React; +let ReactNoop; +let Scheduler; + +describe('ReactClassSetStateCallback', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + }); + + function Text({text}) { + Scheduler.unstable_yieldValue(text); + return text; + } + + test('regression: setState callback (2nd arg) should only fire once, even after a rebase', async () => { + let app; + class App extends React.Component { + state = {step: 0}; + render() { + app = this; + return ; + } + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded([0]); + + await ReactNoop.act(async () => { + app.setState({step: 1}, () => + Scheduler.unstable_yieldValue('Callback 1'), + ); + ReactNoop.flushSync(() => { + app.setState({step: 2}, () => + Scheduler.unstable_yieldValue('Callback 2'), + ); + }); + }); + expect(Scheduler).toHaveYielded([2, 'Callback 2', 2, 'Callback 1']); + }); +});