@samitouri / QOS-React-2 / commits / 2c8a1452b8

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 });