diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
index 360cfa9f9a392..68708258c52c3 100644
--- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
@@ -179,4 +179,44 @@ describe('ReactDOMHooks', () => {
expect(labelRef.current.innerHTML).toBe('abc');
});
+ it('should flush passive effects before interactive events', () => {
+ // related to #15057
+ // the test is a bit contrived since it'll never happen in a 'real' test/browser
+ // (the effect would flush on start and the clicks would never set state)
+ // but it does model setting state in an effect clean up that removes a previous handler
+
+ // users should probably wrap their code with unstable_interactiveUpdates? or
+ // use emitEffect?
+ const {useState, useEffect} = React;
+
+ function Foo() {
+ const [count, setCount] = useState(0);
+ const [enabled, setEnabled] = useState(true);
+ useEffect(() => {
+ return () => {
+ setEnabled(false);
+ };
+ });
+ function handleClick() {
+ setCount(x => x + 1);
+ }
+ return ;
+ }
+
+ ReactDOM.render(, container);
+
+ container.firstChild.dispatchEvent(
+ new Event('click', {bubbles: true, cancelable: true}),
+ );
+ // Cleanup from first passive effect should remove the handler.
+ container.firstChild.dispatchEvent(
+ new Event('click', {bubbles: true, cancelable: true}),
+ );
+ container.firstChild.dispatchEvent(
+ new Event('click', {bubbles: true, cancelable: true}),
+ );
+
+ jest.runAllTimers();
+ expect(container.textContent).toBe('1');
+ });
});
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index 7864aa0f9a56f..95c21c603c210 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -539,6 +539,7 @@ export function flushInteractiveUpdates() {
// an input event inside an effect, like with `element.click()`.
return;
}
+ flushPassiveEffects();
flushPendingDiscreteUpdates();
}
@@ -575,7 +576,7 @@ export function interactiveUpdates(
if (workPhase === NotWorking) {
// TODO: Remove this call. Instead of doing this automatically, the caller
// should explicitly call flushInteractiveUpdates.
- flushPendingDiscreteUpdates();
+ flushInteractiveUpdates();
}
return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
}