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 (
+
{
+ if (!ran) {
+ ran = true;
+ setState(1);
+ simulateSafariBug();
+ }
+ }}>
+ {state}
+
+ );
+ }
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render();
+ });
+ 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.