Repository: facebook/react
Pull request: Fix setState ignored in Safari when iframe is added to DOM in the same commit

Fixes https://github.com/facebook/react/issues/22459.

I'm not confident this is the best place to fix it.

Here's a minimal repro:

```js
document.getElementById("app").innerHTML = `
<button onclick="addIframe()">add iframe</button>
`;

window.addIframe = function addIframe() {
  queueMicrotask(() => {
    console.log("--- in microtask ---");
  });
  console.log("will add iframe");
  const iframe = document.createElement("iframe");
  iframe.src = "localhost";
  document.body.appendChild(iframe);
  console.log("did add iframe");
};
```

https://codesandbox.io/s/fervent-turing-7rqyl?file=/src/index.js

In Chrome and FF, the order of the logs is:

```
will add iframe 
did add iframe 
--- in microtask --- 
```

In Safari, the order is:

```
will add iframe 
--- in microtask --- 
did add iframe 
```

So microtasks don't necessarily run on a clean stack. (Doesn't this violate the spec?)

Since this breaks our assumptions, my strawfix is to ignore the callback if it's called at a bad time. However, I'm not sure it's sufficient. (Are we guaranteed to always flush them at the end anyway? I think so.)

Note that to debug this locally you'll need to serve from http. I couldn't repro with file protocol.

Test plan: Sandbox from https://github.com/facebook/react/issues/22459 works now.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js
patch: @@ -0,0 +1,71 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+
+let ReactDOM;
+let act;
+
+describe('ReactDOMSafariMicrotaskBug-test', () => {
+  let container;
+  let simulateSafariBug;
+
+  beforeEach(() => {
+    // In Safari, microtasks don't always run on clean stack.
+    // This setup crudely approximates it.
+    // In reality, the sync flush happens when an iframe is added to the page.
+    // https://github.com/facebook/react/issues/22459
+    let queue = [];
+    window.queueMicrotask = function(cb) {
+      queue.push(cb);
+    };
+    simulateSafariBug = function() {
+      queue.forEach(cb => cb());
+      queue = [];
+    };
+
+    jest.resetModules();
+    container = document.createElement('div');
+    React = require('react');
+    ReactDOM = require('react-dom');
+    act = require('jest-react').act;
+
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(container);
+  });
+
+  it('should be resilient to buggy queueMicrotask', async () => {
+    let ran = false;
+    function Foo() {
+      const [state, setState] = React.useState(0);
+      return (
+        <div
+          ref={() => {
+            if (!ran) {
+              ran = true;
+              setState(1);
+              simulateSafariBug();
+            }
+          }}>
+          {state}
+        </div>
+      );
+    }
+    const root = ReactDOM.createRoot(container);
+    await act(async () => {
+      root.render(<Foo />);
+    });
+    expect(container.textContent).toBe('1');
+  });
+});

filename: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
patch: @@ -708,7 +708,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
         // of `act`.
         ReactCurrentActQueue.current.push(flushSyncCallbacks);
       } else {
-        scheduleMicrotask(flushSyncCallbacks);
+        scheduleMicrotask(() => {
+          // In Safari, appending an iframe forces microtasks to run.
+          // https://github.com/facebook/react/issues/22459
+          // We don't support running callbacks in the middle of render
+          // or commit so we need to check against that.
+          if (executionContext === NoContext) {
+            // It's only safe to do this conditionally because we always
+            // check for pending work before we exit the task.
+            flushSyncCallbacks();
+          }
+        });
       }
     } else {
       // Flush the queue in an Immediate task.

filename: packages/react-reconciler/src/ReactFiberWorkLoop.old.js
patch: @@ -708,7 +708,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
         // of `act`.
         ReactCurrentActQueue.current.push(flushSyncCallbacks);
       } else {
-        scheduleMicrotask(flushSyncCallbacks);
+        scheduleMicrotask(() => {
+          // In Safari, appending an iframe forces microtasks to run.
+          // https://github.com/facebook/react/issues/22459
+          // We don't support running callbacks in the middle of render
+          // or commit so we need to check against that.
+          if (executionContext === NoContext) {
+            // It's only safe to do this conditionally because we always
+            // check for pending work before we exit the task.
+            flushSyncCallbacks();
+          }
+        });
       }
     } else {
       // Flush the queue in an Immediate task.