Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ignored setState in Safari when iframe is touched #24459

Merged
merged 1 commit into from May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -16,7 +16,7 @@ let act;

describe('ReactDOMSafariMicrotaskBug-test', () => {
let container;
let simulateSafariBug;
let flushMicrotasksPrematurely;

beforeEach(() => {
// In Safari, microtasks don't always run on clean stack.
Expand All @@ -27,9 +27,12 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
window.queueMicrotask = function(cb) {
queue.push(cb);
};
simulateSafariBug = function() {
queue.forEach(cb => cb());
queue = [];
flushMicrotasksPrematurely = function() {
while (queue.length > 0) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This simulates the Safari behavior of trying to drain the queue until it's empty. So that we don't "fix" this by adding an infinite loop.

Proof:

<body>
<script>
let busy = true
const callback = () => {
  if (busy) {
    console.error('noo im busy')
    queueMicrotask(callback)
  } else {
    console.log('im ok')
  }
}
queueMicrotask(callback);
console.log("will add iframe");
const iframe = document.createElement("iframe");
iframe.src = "localhost";
document.body.appendChild(iframe);
console.log("did add iframe");
busy = false
</script>
</body>

gets Safari infinitely stuck

const prevQueue = queue;
queue = [];
prevQueue.forEach(cb => cb());
}
};

jest.resetModules();
Expand All @@ -45,7 +48,7 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
document.body.removeChild(container);
});

it('should be resilient to buggy queueMicrotask', async () => {
it('should deal with premature microtask in commit phase', async () => {
let ran = false;
function Foo() {
const [state, setState] = React.useState(0);
Expand All @@ -55,7 +58,7 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
if (!ran) {
ran = true;
setState(1);
simulateSafariBug();
flushMicrotasksPrematurely();
}
}}>
{state}
Expand All @@ -68,4 +71,30 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
});
expect(container.textContent).toBe('1');
});

it('should deal with premature microtask in event handler', async () => {
function Foo() {
const [state, setState] = React.useState(0);
return (
<button
onClick={() => {
setState(1);
flushMicrotasksPrematurely();
}}>
{state}
</button>
);
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Foo />);
});
expect(container.textContent).toBe('0');
await act(async () => {
container.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
});
expect(container.textContent).toBe('1');
});
});
9 changes: 6 additions & 3 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Expand Up @@ -834,9 +834,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 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.
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).
flushSyncCallbacks();
}
});
Expand Down
9 changes: 6 additions & 3 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Expand Up @@ -834,9 +834,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 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.
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).
flushSyncCallbacks();
}
});
Expand Down