@samitouri / QOS-React / commits / e8cdce40d6

Don't flush sync at end of discreteUpdates (#21327)

All it should do is change the priority. The updates will be flushed by the microtask.

Andrew Clark committed Apr 22, 2021 at 15:28 UTC e8cdce40d667fa6745c9412b764c65eb85435b97
6 files changed +41 -132
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+20 -12
@@ -427,7 +427,7 @@ describe('ReactDOMFiberAsync', () => {
427 });
428
429 // @gate experimental
430 - it('ignores discrete events on a pending removed event listener', () => {
430 + it('ignores discrete events on a pending removed event listener', async () => {
431 const disableButtonRef = React.createRef();
432 const submitButtonRef = React.createRef();
433
@@ -459,9 +459,9 @@ describe('ReactDOMFiberAsync', () => {
459 }
460
461 const root = ReactDOM.unstable_createRoot(container);
462 - root.render(<Form />);
463 - // Flush
464 - Scheduler.unstable_flushAll();
462 + await act(async () => {
463 + root.render(<Form />);
464 + });
465
466 const disableButton = disableButtonRef.current;
467 expect(disableButton.tagName).toBe('BUTTON');
@@ -469,7 +469,9 @@ describe('ReactDOMFiberAsync', () => {
469 // Dispatch a click event on the Disable-button.
470 const firstEvent = document.createEvent('Event');
471 firstEvent.initEvent('click', true, true);
472 - disableButton.dispatchEvent(firstEvent);
472 + await act(async () => {
473 + disableButton.dispatchEvent(firstEvent);
474 + });
475
476 // There should now be a pending update to disable the form.
477
@@ -481,14 +483,16 @@ describe('ReactDOMFiberAsync', () => {
483 const secondEvent = document.createEvent('Event');
484 secondEvent.initEvent('click', true, true);
485 // This should force the pending update to flush which disables the submit button before the event is invoked.
484 - submitButton.dispatchEvent(secondEvent);
486 + await act(async () => {
487 + submitButton.dispatchEvent(secondEvent);
488 + });
489
490 // Therefore the form should never have been submitted.
491 expect(formSubmitted).toBe(false);
492 });
493
494 // @gate experimental
491 - it('uses the newest discrete events on a pending changed event listener', () => {
495 + it('uses the newest discrete events on a pending changed event listener', async () => {
496 const enableButtonRef = React.createRef();
497 const submitButtonRef = React.createRef();
498
@@ -515,9 +519,9 @@ describe('ReactDOMFiberAsync', () => {
519 }
520
521 const root = ReactDOM.unstable_createRoot(container);
518 - root.render(<Form />);
519 - // Flush
520 - Scheduler.unstable_flushAll();
522 + await act(async () => {
523 + root.render(<Form />);
524 + });
525
526 const enableButton = enableButtonRef.current;
527 expect(enableButton.tagName).toBe('BUTTON');
@@ -525,7 +529,9 @@ describe('ReactDOMFiberAsync', () => {
529 // Dispatch a click event on the Enable-button.
530 const firstEvent = document.createEvent('Event');
531 firstEvent.initEvent('click', true, true);
528 - enableButton.dispatchEvent(firstEvent);
532 + await act(async () => {
533 + enableButton.dispatchEvent(firstEvent);
534 + });
535
536 // There should now be a pending update to enable the form.
537
@@ -537,7 +543,9 @@ describe('ReactDOMFiberAsync', () => {
543 const secondEvent = document.createEvent('Event');
544 secondEvent.initEvent('click', true, true);
545 // This should force the pending update to flush which enables the submit button before the event is invoked.
540 - submitButton.dispatchEvent(secondEvent);
546 + await act(async () => {
547 + submitButton.dispatchEvent(secondEvent);
548 + });
549
550 // Therefore the form should have been submitted.
551 expect(formSubmitted).toBe(true);
packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
+4 -5
@@ -357,6 +357,9 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
357 const pressEvent = document.createEvent('Event');
358 pressEvent.initEvent('click', true, true);
359 dispatchAndSetCurrentEvent(target.current, pressEvent);
360 + // Intentionally not using `act` so we can observe in between the press
361 + // event and the microtask, without batching.
362 + await null;
363 // If this is 2, that means the `setCount` calls were not batched.
364 expect(container.textContent).toEqual('Count: 1');
365
@@ -409,11 +412,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
412 dispatchAndSetCurrentEvent(target, pressEvent);
413
414 expect(Scheduler).toHaveYielded(['Count: 0 [after batchedUpdates]']);
412 - // TODO: There's a `flushDiscreteUpdates` call at the end of the event
413 - // delegation listener that gets called even if no React event handlers are
414 - // fired. Once that is removed, this will be 0, not 1.
415 - // expect(container.textContent).toEqual('Count: 0');
416 - expect(container.textContent).toEqual('Count: 1');
415 + expect(container.textContent).toEqual('Count: 0');
416
417 // Intentionally not using `act` so we can observe in between the click
418 // event and the microtask, without batching.
packages/react-dom/src/events/ReactDOMUpdateBatching.js
+2
@@ -39,6 +39,8 @@ function finishEventHandler() {
39 // If a controlled event was fired, we may need to restore the state of
40 // the DOM node back to the controlled value. This is necessary when React
41 // bails out of the update without touching the DOM.
42 + // TODO: Restore state in the microtask, after the discrete updates flush,
43 + // instead of early flushing them here.
44 flushDiscreteUpdatesImpl();
45 restoreStateIfNeeded();
46 }
packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
+15 -109
@@ -13,7 +13,7 @@ describe('SimpleEventPlugin', function() {
13 let React;
14 let ReactDOM;
15 let Scheduler;
16 - let TestUtils;
16 + let act;
17
18 let onClick;
19 let container;
@@ -40,7 +40,6 @@ describe('SimpleEventPlugin', function() {
40 React = require('react');
41 ReactDOM = require('react-dom');
42 Scheduler = require('scheduler');
43 - TestUtils = require('react-dom/test-utils');
43
44 onClick = jest.fn();
45 });
@@ -237,10 +236,12 @@ describe('SimpleEventPlugin', function() {
236 React = require('react');
237 ReactDOM = require('react-dom');
238 Scheduler = require('scheduler');
239 +
240 + act = require('react-dom/test-utils').unstable_concurrentAct;
241 });
242
243 // @gate experimental
243 - it('flushes pending interactive work before exiting event handler', () => {
244 + it('flushes pending interactive work before exiting event handler', async () => {
245 container = document.createElement('div');
246 const root = ReactDOM.unstable_createRoot(container);
247 document.body.appendChild(container);
@@ -288,7 +289,7 @@ describe('SimpleEventPlugin', function() {
289 }
290
291 // Click the button to trigger the side-effect
291 - click();
292 + await act(async () => click());
293 expect(Scheduler).toHaveYielded([
294 // The handler fired
295 'Side-effect',
@@ -312,6 +313,9 @@ describe('SimpleEventPlugin', function() {
313 expect(Scheduler).toFlushAndYield([]);
314 });
315
316 + // NOTE: This test was written for the old behavior of discrete updates,
317 + // where they would be async, but flushed early if another discrete update
318 + // was dispatched.
319 // @gate experimental
320 it('end result of many interactive updates is deterministic', async () => {
321 container = document.createElement('div');
@@ -355,121 +359,23 @@ describe('SimpleEventPlugin', function() {
359 }
360
361 // Click the button a single time
358 - click();
362 + await act(async () => click());
363 // The counter should update synchronously, even in concurrent mode.
364 expect(button.textContent).toEqual('Count: 1');
365
366 // Click the button many more times
363 - await TestUtils.act(async () => {
364 - click();
365 - click();
366 - click();
367 - click();
368 - click();
369 - click();
370 - });
367 + await act(async () => click());
368 + await act(async () => click());
369 + await act(async () => click());
370 + await act(async () => click());
371 + await act(async () => click());
372 + await act(async () => click());
373
374 // Flush the remaining work
375 Scheduler.unstable_flushAll();
376 // The counter should equal the total number of clicks
377 expect(button.textContent).toEqual('Count: 7');
378 });
377 -
378 - // @gate experimental
379 - it('flushes discrete updates in order', async () => {
380 - container = document.createElement('div');
381 - document.body.appendChild(container);
382 -
383 - let button;
384 - class Button extends React.Component {
385 - state = {lowPriCount: 0};
386 - render() {
387 - const text = `High-pri count: ${this.props.highPriCount}, Low-pri count: ${this.state.lowPriCount}`;
388 - Scheduler.unstable_yieldValue(text);
389 - return (
390 - <button
391 - ref={el => (button = el)}
392 - onClick={() => {
393 - React.unstable_startTransition(() => {
394 - this.setState(state => ({
395 - lowPriCount: state.lowPriCount + 1,
396 - }));
397 - });
398 - }}>
399 - {text}
400 - </button>
401 - );
402 - }
403 - }
404 -
405 - class Wrapper extends React.Component {
406 - state = {highPriCount: 0};
407 - render() {
408 - return (
409 - <div
410 - onClick={
411 - // Intentionally not using the updater form here, to test
412 - // that updates are serially processed.
413 - () => {
414 - this.setState({highPriCount: this.state.highPriCount + 1});
415 - }
416 - }>
417 - <Button highPriCount={this.state.highPriCount} />
418 - </div>
419 - );
420 - }
421 - }
422 -
423 - // Initial mount
424 - const root = ReactDOM.unstable_createRoot(container);
425 - root.render(<Wrapper />);
426 - expect(Scheduler).toFlushAndYield([
427 - 'High-pri count: 0, Low-pri count: 0',
428 - ]);
429 - expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
430 -
431 - function click() {
432 - const event = new MouseEvent('click', {
433 - bubbles: true,
434 - cancelable: true,
435 - });
436 - Object.defineProperty(event, 'timeStamp', {
437 - value: 0,
438 - });
439 - button.dispatchEvent(event);
440 - }
441 -
442 - // Click the button a single time.
443 - // This will flush at the end of the event, even in concurrent mode.
444 - click();
445 - expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
446 -
447 - // Click the button many more times
448 - click();
449 - click();
450 - click();
451 - click();
452 - click();
453 - click();
454 -
455 - // Each update should synchronously flush, even in concurrent mode.
456 - expect(Scheduler).toHaveYielded([
457 - 'High-pri count: 2, Low-pri count: 0',
458 - 'High-pri count: 3, Low-pri count: 0',
459 - 'High-pri count: 4, Low-pri count: 0',
460 - 'High-pri count: 5, Low-pri count: 0',
461 - 'High-pri count: 6, Low-pri count: 0',
462 - 'High-pri count: 7, Low-pri count: 0',
463 - ]);
464 -
465 - // Now flush the scheduler to apply the transition updates.
466 - // At the end, both counters should equal the total number of clicks.
467 - expect(Scheduler).toFlushAndYield([
468 - 'High-pri count: 7, Low-pri count: 7',
469 - ]);
470 -
471 - expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
472 - });
379 });
380
381 describe('iOS bubbling click fix', function() {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
-3
@@ -1160,9 +1160,6 @@ export function discreteUpdates<A, B, C, D, R>(
1160 ReactCurrentBatchConfig.transition = prevTransition;
1161 if (executionContext === NoContext) {
1162 resetRenderTimer();
1163 - // TODO: This should only flush legacy sync updates. Not discrete updates
1164 - // in Concurrent Mode. Discrete updates will flush in a microtask.
1165 - flushSyncCallbacks();
1163 }
1164 }
1165 }
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
-3
@@ -1160,9 +1160,6 @@ export function discreteUpdates<A, B, C, D, R>(
1160 ReactCurrentBatchConfig.transition = prevTransition;
1161 if (executionContext === NoContext) {
1162 resetRenderTimer();
1163 - // TODO: This should only flush legacy sync updates. Not discrete updates
1164 - // in Concurrent Mode. Discrete updates will flush in a microtask.
1165 - flushSyncCallbacks();
1163 }
1164 }
1165 }