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.