Fix ignored setState in Safari when iframe is touched (#24459)
dan committed
May 12, 2022 at 17:58 UTC
2c8a1452b82b9ec5ebfa3f370b31fda19610ae92
3 files changed
+47
-12
packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js
+35
-6
@@ -16,7 +16,7 @@ let act;
16
17
describe('ReactDOMSafariMicrotaskBug-test', () => {
18
let container;
19
- let simulateSafariBug;
19
+ let flushMicrotasksPrematurely;
20
21
beforeEach(() => {
22
// In Safari, microtasks don't always run on clean stack.
@@ -27,9 +27,12 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
27
window.queueMicrotask = function(cb) {
28
queue.push(cb);
29
};
30
- simulateSafariBug = function() {
31
- queue.forEach(cb => cb());
32
- queue = [];
30
+ flushMicrotasksPrematurely = function() {
31
+ while (queue.length > 0) {
32
+ const prevQueue = queue;
33
+ queue = [];
34
+ prevQueue.forEach(cb => cb());
35
+ }
36
};
37
38
jest.resetModules();
@@ -45,7 +48,7 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
48
document.body.removeChild(container);
49
});
50
48
- it('should be resilient to buggy queueMicrotask', async () => {
51
+ it('should deal with premature microtask in commit phase', async () => {
52
let ran = false;
53
function Foo() {
54
const [state, setState] = React.useState(0);
@@ -55,7 +58,7 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
58
if (!ran) {
59
ran = true;
60
setState(1);
58
- simulateSafariBug();
61
+ flushMicrotasksPrematurely();
62
}
63
}}>
64
{state}
@@ -68,4 +71,30 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
71
});
72
expect(container.textContent).toBe('1');
73
});
74
+
75
+ it('should deal with premature microtask in event handler', async () => {
76
+ function Foo() {
77
+ const [state, setState] = React.useState(0);
78
+ return (
79
+ <button
80
+ onClick={() => {
81
+ setState(1);
82
+ flushMicrotasksPrematurely();
83
+ }}>
84
+ {state}
85
+ </button>
86
+ );
87
+ }
88
+ const root = ReactDOMClient.createRoot(container);
89
+ await act(async () => {
90
+ root.render(<Foo />);
91
+ });
92
+ expect(container.textContent).toBe('0');
93
+ await act(async () => {
94
+ container.firstChild.dispatchEvent(
95
+ new MouseEvent('click', {bubbles: true}),
96
+ );
97
+ });
98
+ expect(container.textContent).toBe('1');
99
+ });
100
});
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+6
-3
@@ -835,9 +835,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
835
// https://github.com/facebook/react/issues/22459
836
// We don't support running callbacks in the middle of render
837
// or commit so we need to check against that.
838
- if (executionContext === NoContext) {
839
- // It's only safe to do this conditionally because we always
840
- // check for pending work before we exit the task.
838
+ if (
839
+ (executionContext & (RenderContext | CommitContext)) ===
840
+ NoContext
841
+ ) {
842
+ // Note that this would still prematurely flush the callbacks
843
+ // if this happens outside render or commit phase (e.g. in an event).
844
flushSyncCallbacks();
845
}
846
});
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+6
-3
@@ -835,9 +835,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
835
// https://github.com/facebook/react/issues/22459
836
// We don't support running callbacks in the middle of render
837
// or commit so we need to check against that.
838
- if (executionContext === NoContext) {
839
- // It's only safe to do this conditionally because we always
840
- // check for pending work before we exit the task.
838
+ if (
839
+ (executionContext & (RenderContext | CommitContext)) ===
840
+ NoContext
841
+ ) {
842
+ // Note that this would still prematurely flush the callbacks
843
+ // if this happens outside render or commit phase (e.g. in an event).
844
flushSyncCallbacks();
845
}
846
});