diff --git a/packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js b/packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js
new file mode 100644
index 0000000000000..1e12bb611ca78
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js
@@ -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');
+  });
+});
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index b108da0106e42..b4b333547c194 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -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.
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index a32f8853337e6..d8bb61af50c84 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -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.