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
}