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.