diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
index e89eebb491576..15515672747a6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
@@ -9,7 +9,7 @@
'use strict';
-const React = require('react');
+let React;
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let ReactDOM;
@@ -26,6 +26,7 @@ describe('ReactDOMFiberAsync', () => {
beforeEach(() => {
jest.resetModules();
container = document.createElement('div');
+ React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
@@ -587,6 +588,39 @@ describe('ReactDOMFiberAsync', () => {
);
});
+ it('regression test: does not drop passive effects across roots (#17066)', () => {
+ const {useState, useEffect} = React;
+
+ function App({label}) {
+ const [step, setStep] = useState(0);
+ useEffect(
+ () => {
+ if (step < 3) {
+ setStep(step + 1);
+ }
+ },
+ [step],
+ );
+
+ // The component should keep re-rendering itself until `step` is 3.
+ return step === 3 ? 'Finished' : 'Unresolved';
+ }
+
+ const containerA = document.createElement('div');
+ const containerB = document.createElement('div');
+ const containerC = document.createElement('div');
+
+ ReactDOM.render(, containerA);
+ ReactDOM.render(, containerB);
+ ReactDOM.render(, containerC);
+
+ Scheduler.unstable_flushAll();
+
+ expect(containerA.textContent).toEqual('Finished');
+ expect(containerB.textContent).toEqual('Finished');
+ expect(containerC.textContent).toEqual('Finished');
+ });
+
describe('createBlockingRoot', () => {
it.experimental('updates flush without yielding in the next event', () => {
const root = ReactDOM.createBlockingRoot(container);
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 85d678dec501f..954d646bd789f 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -1713,7 +1713,15 @@ function commitRoot(root) {
}
function commitRootImpl(root, renderPriorityLevel) {
- flushPassiveEffects();
+ do {
+ // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
+ // means `flushPassiveEffects` will sometimes result in additional
+ // passive effects. So we need to keep flushing in a loop until there are
+ // no more pending effects.
+ // TODO: Might be better if `flushPassiveEffects` did not automatically
+ // flush synchronous work at the end, to avoid factoring hazards like this.
+ flushPassiveEffects();
+ } while (rootWithPendingPassiveEffects !== null);
flushRenderPhaseStrictModeWarningsInDEV();
invariant(