@samitouri / QOS-React / commits / 790b5246f6

Fix setState ignored in Safari when iframe is added to DOM in the same commit (#23111)

* Fix setState being ignored in Safari * Add a regression test * Add comment

Dan Abramov committed Jan 18, 2022 at 18:25 UTC 790b5246f691adafbf4b6a4b3fe2e6cc1370c43e
3 files changed +93 -2
packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js new
+71
@@ -0,0 +1,71 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +
14 +let ReactDOM;
15 +let act;
16 +
17 +describe('ReactDOMSafariMicrotaskBug-test', () => {
18 + let container;
19 + let simulateSafariBug;
20 +
21 + beforeEach(() => {
22 + // In Safari, microtasks don't always run on clean stack.
23 + // This setup crudely approximates it.
24 + // In reality, the sync flush happens when an iframe is added to the page.
25 + // https://github.com/facebook/react/issues/22459
26 + let queue = [];
27 + window.queueMicrotask = function(cb) {
28 + queue.push(cb);
29 + };
30 + simulateSafariBug = function() {
31 + queue.forEach(cb => cb());
32 + queue = [];
33 + };
34 +
35 + jest.resetModules();
36 + container = document.createElement('div');
37 + React = require('react');
38 + ReactDOM = require('react-dom');
39 + act = require('jest-react').act;
40 +
41 + document.body.appendChild(container);
42 + });
43 +
44 + afterEach(() => {
45 + document.body.removeChild(container);
46 + });
47 +
48 + it('should be resilient to buggy queueMicrotask', async () => {
49 + let ran = false;
50 + function Foo() {
51 + const [state, setState] = React.useState(0);
52 + return (
53 + <div
54 + ref={() => {
55 + if (!ran) {
56 + ran = true;
57 + setState(1);
58 + simulateSafariBug();
59 + }
60 + }}>
61 + {state}
62 + </div>
63 + );
64 + }
65 + const root = ReactDOM.createRoot(container);
66 + await act(async () => {
67 + root.render(<Foo />);
68 + });
69 + expect(container.textContent).toBe('1');
70 + });
71 +});
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+11 -1
@@ -708,7 +708,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
708 // of `act`.
709 ReactCurrentActQueue.current.push(flushSyncCallbacks);
710 } else {
711 - scheduleMicrotask(flushSyncCallbacks);
711 + scheduleMicrotask(() => {
712 + // In Safari, appending an iframe forces microtasks to run.
713 + // https://github.com/facebook/react/issues/22459
714 + // We don't support running callbacks in the middle of render
715 + // or commit so we need to check against that.
716 + if (executionContext === NoContext) {
717 + // It's only safe to do this conditionally because we always
718 + // check for pending work before we exit the task.
719 + flushSyncCallbacks();
720 + }
721 + });
722 }
723 } else {
724 // Flush the queue in an Immediate task.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+11 -1
@@ -708,7 +708,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
708 // of `act`.
709 ReactCurrentActQueue.current.push(flushSyncCallbacks);
710 } else {
711 - scheduleMicrotask(flushSyncCallbacks);
711 + scheduleMicrotask(() => {
712 + // In Safari, appending an iframe forces microtasks to run.
713 + // https://github.com/facebook/react/issues/22459
714 + // We don't support running callbacks in the middle of render
715 + // or commit so we need to check against that.
716 + if (executionContext === NoContext) {
717 + // It's only safe to do this conditionally because we always
718 + // check for pending work before we exit the task.
719 + flushSyncCallbacks();
720 + }
721 + });
722 }
723 } else {
724 // Flush the queue in an Immediate task.