Revert "Cleanup enableSyncDefaultUpdate flag (#26236)" (#26528)
This reverts commit b2ae9ddb3b497d16a7c27c051da1827d08871138. While the feature flag is fully rolled out, these tests are also testing behavior set with an unstable flag on root, which for now we want to preserve. Not sure if there's a better way then adding a dynamic feature flag to the www build?
Jan Kassens committed
Apr 4, 2023 at 10:08 UTC
da94e8b24a3f31a3e805f9bf6bba73055aad9d41
36 files changed
+1477
-314
packages/react-art/src/__tests__/ReactART-test.js
+58
@@ -33,12 +33,16 @@ const ReactTestRenderer = require('react-test-renderer');
33
34
// Isolate the noop renderer
35
jest.resetModules();
36
+const ReactNoop = require('react-noop-renderer');
37
+const Scheduler = require('scheduler');
38
39
let Group;
40
let Shape;
41
let Surface;
42
let TestComponent;
43
44
+let waitFor;
45
+
46
const Missing = {};
47
48
function testDOMNodeStructure(domNode, expectedStructure) {
@@ -76,6 +80,8 @@ describe('ReactART', () => {
80
Shape = ReactART.Shape;
81
Surface = ReactART.Surface;
82
83
+ ({waitFor} = require('internal-test-utils'));
84
+
85
TestComponent = class extends React.Component {
86
group = React.createRef();
87
@@ -357,6 +363,58 @@ describe('ReactART', () => {
363
doClick(instance);
364
expect(onClick2).toBeCalled();
365
});
366
+
367
+ // @gate !enableSyncDefaultUpdates
368
+ it('can concurrently render with a "primary" renderer while sharing context', async () => {
369
+ const CurrentRendererContext = React.createContext(null);
370
+
371
+ function Yield(props) {
372
+ Scheduler.log(props.value);
373
+ return null;
374
+ }
375
+
376
+ let ops = [];
377
+ function LogCurrentRenderer() {
378
+ return (
379
+ <CurrentRendererContext.Consumer>
380
+ {currentRenderer => {
381
+ ops.push(currentRenderer);
382
+ return null;
383
+ }}
384
+ </CurrentRendererContext.Consumer>
385
+ );
386
+ }
387
+
388
+ // Using test renderer instead of the DOM renderer here because async
389
+ // testing APIs for the DOM renderer don't exist.
390
+ ReactNoop.render(
391
+ <CurrentRendererContext.Provider value="Test">
392
+ <Yield value="A" />
393
+ <Yield value="B" />
394
+ <LogCurrentRenderer />
395
+ <Yield value="C" />
396
+ </CurrentRendererContext.Provider>,
397
+ );
398
+
399
+ await waitFor(['A']);
400
+
401
+ ReactDOM.render(
402
+ <Surface>
403
+ <LogCurrentRenderer />
404
+ <CurrentRendererContext.Provider value="ART">
405
+ <LogCurrentRenderer />
406
+ </CurrentRendererContext.Provider>
407
+ </Surface>,
408
+ container,
409
+ );
410
+
411
+ expect(ops).toEqual([null, 'ART']);
412
+
413
+ ops = [];
414
+ await waitFor(['B', 'C']);
415
+
416
+ expect(ops).toEqual(['Test']);
417
+ });
418
});
419
420
describe('ReactARTComponents', () => {
packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
+5
-1
@@ -312,7 +312,11 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
312
expect(container.textContent).toEqual('not hovered');
313
314
await waitFor(['hovered']);
315
- expect(container.textContent).toEqual('hovered');
315
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
316
+ expect(container.textContent).toEqual('hovered');
317
+ } else {
318
+ expect(container.textContent).toEqual('not hovered');
319
+ }
320
});
321
expect(container.textContent).toEqual('hovered');
322
});
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+8
-1
@@ -2036,7 +2036,14 @@ describe('ReactDOMServerPartialHydration', () => {
2036
suspend = true;
2037
2038
await act(async () => {
2039
- await waitFor(['Before', 'After']);
2039
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2040
+ await waitFor(['Before', 'After']);
2041
+ } else {
2042
+ await waitFor(['Before']);
2043
+ // This took a long time to render.
2044
+ Scheduler.unstable_advanceTime(1000);
2045
+ await waitFor(['After']);
2046
+ }
2047
2048
// This will cause us to skip the second row completely.
2049
});
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+6
-2
@@ -1984,9 +1984,13 @@ describe('DOMPluginEventSystem', () => {
1984
log.length = 0;
1985
1986
// Increase counter
1987
- React.startTransition(() => {
1987
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1988
+ React.startTransition(() => {
1989
+ root.render(<Test counter={1} />);
1990
+ });
1991
+ } else {
1992
root.render(<Test counter={1} />);
1989
- });
1993
+ }
1994
// Yield before committing
1995
await waitFor(['Test']);
1996
packages/react-reconciler/src/ReactFiber.js
+5
-2
@@ -33,6 +33,7 @@ import {
33
enableProfilerTimer,
34
enableScopeAPI,
35
enableLegacyHidden,
36
+ enableSyncDefaultUpdates,
37
allowConcurrentByDefault,
38
enableTransitionTracing,
39
enableDebugTracing,
@@ -458,9 +459,11 @@ export function createHostRootFiber(
459
mode |= StrictLegacyMode | StrictEffectsMode;
460
}
461
if (
462
+ // We only use this flag for our repo tests to check both behaviors.
463
+ // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting"
464
+ !enableSyncDefaultUpdates ||
465
// Only for internal experiments.
462
- allowConcurrentByDefault &&
463
- concurrentUpdatesByDefaultOverride
466
+ (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
467
) {
468
mode |= ConcurrentUpdatesByDefaultMode;
469
}
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+153
-62
@@ -115,29 +115,54 @@ describe('ReactExpiration', () => {
115
}
116
}
117
118
+ function flushNextRenderIfExpired() {
119
+ // This will start rendering the next level of work. If the work hasn't
120
+ // expired yet, React will exit without doing anything. If it has expired,
121
+ // it will schedule a sync task.
122
+ Scheduler.unstable_flushExpired();
123
+ // Flush the sync task.
124
+ ReactNoop.flushSync();
125
+ }
126
+
127
it('increases priority of updates as time progresses', async () => {
119
- ReactNoop.render(<Text text="Step 1" />);
120
- React.startTransition(() => {
121
- ReactNoop.render(<Text text="Step 2" />);
122
- });
128
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
129
+ ReactNoop.render(<Text text="Step 1" />);
130
+ React.startTransition(() => {
131
+ ReactNoop.render(<Text text="Step 2" />);
132
+ });
133
+ await waitFor(['Step 1']);
134
124
- await waitFor(['Step 1']);
135
+ expect(ReactNoop).toMatchRenderedOutput('Step 1');
136
126
- expect(ReactNoop).toMatchRenderedOutput('Step 1');
137
+ // Nothing has expired yet because time hasn't advanced.
138
+ await unstable_waitForExpired([]);
139
+ expect(ReactNoop).toMatchRenderedOutput('Step 1');
140
128
- // Nothing has expired yet because time hasn't advanced.
129
- await unstable_waitForExpired([]);
130
- expect(ReactNoop).toMatchRenderedOutput('Step 1');
141
+ // Advance time a bit, but not enough to expire the low pri update.
142
+ ReactNoop.expire(4500);
143
+ await unstable_waitForExpired([]);
144
+ expect(ReactNoop).toMatchRenderedOutput('Step 1');
145
132
- // Advance time a bit, but not enough to expire the low pri update.
133
- ReactNoop.expire(4500);
134
- await unstable_waitForExpired([]);
135
- expect(ReactNoop).toMatchRenderedOutput('Step 1');
146
+ // Advance by a little bit more. Now the update should expire and flush.
147
+ ReactNoop.expire(500);
148
+ await unstable_waitForExpired(['Step 2']);
149
+ expect(ReactNoop).toMatchRenderedOutput('Step 2');
150
+ } else {
151
+ ReactNoop.render(<span prop="done" />);
152
+ expect(ReactNoop).toMatchRenderedOutput(null);
153
137
- // Advance by a little bit more. Now the update should expire and flush.
138
- ReactNoop.expire(500);
139
- await unstable_waitForExpired(['Step 2']);
140
- expect(ReactNoop).toMatchRenderedOutput('Step 2');
154
+ // Nothing has expired yet because time hasn't advanced.
155
+ flushNextRenderIfExpired();
156
+ expect(ReactNoop).toMatchRenderedOutput(null);
157
+ // Advance time a bit, but not enough to expire the low pri update.
158
+ ReactNoop.expire(4500);
159
+ flushNextRenderIfExpired();
160
+ expect(ReactNoop).toMatchRenderedOutput(null);
161
+ // Advance by another second. Now the update should expire and flush.
162
+ ReactNoop.expire(500);
163
+ flushNextRenderIfExpired();
164
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="done" />);
165
+ }
166
});
167
168
it('two updates of like priority in the same event always flush within the same batch', async () => {
@@ -162,9 +187,13 @@ describe('ReactExpiration', () => {
187
188
// First, show what happens for updates in two separate events.
189
// Schedule an update.
165
- React.startTransition(() => {
190
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
191
+ React.startTransition(() => {
192
+ ReactNoop.render(<TextClass text="A" />);
193
+ });
194
+ } else {
195
ReactNoop.render(<TextClass text="A" />);
167
- });
196
+ }
197
// Advance the timer.
198
Scheduler.unstable_advanceTime(2000);
199
// Partially flush the first update, then interrupt it.
@@ -219,9 +248,13 @@ describe('ReactExpiration', () => {
248
249
// First, show what happens for updates in two separate events.
250
// Schedule an update.
222
- React.startTransition(() => {
251
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
252
+ React.startTransition(() => {
253
+ ReactNoop.render(<TextClass text="A" />);
254
+ });
255
+ } else {
256
ReactNoop.render(<TextClass text="A" />);
224
- });
257
+ }
258
// Advance the timer.
259
Scheduler.unstable_advanceTime(2000);
260
// Partially flush the first update, then interrupt it.
@@ -287,9 +320,13 @@ describe('ReactExpiration', () => {
320
}
321
322
// Initial mount
290
- React.startTransition(() => {
323
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
324
+ React.startTransition(() => {
325
+ ReactNoop.render(<App />);
326
+ });
327
+ } else {
328
ReactNoop.render(<App />);
292
- });
329
+ }
330
await waitForAll([
331
'initial [A] [render]',
332
'initial [B] [render]',
@@ -302,9 +339,13 @@ describe('ReactExpiration', () => {
339
]);
340
341
// Partial update
305
- React.startTransition(() => {
342
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
343
+ React.startTransition(() => {
344
+ subscribers.forEach(s => s.setState({text: '1'}));
345
+ });
346
+ } else {
347
subscribers.forEach(s => s.setState({text: '1'}));
307
- });
348
+ }
349
await waitFor(['1 [A] [render]', '1 [B] [render]']);
350
351
// Before the update can finish, update again. Even though no time has
@@ -330,9 +371,13 @@ describe('ReactExpiration', () => {
371
);
372
}
373
333
- React.startTransition(() => {
374
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
375
+ React.startTransition(() => {
376
+ root.render(<App />);
377
+ });
378
+ } else {
379
root.render(<App />);
335
- });
380
+ }
381
382
await waitFor(['A']);
383
await waitFor(['B']);
@@ -359,9 +404,13 @@ describe('ReactExpiration', () => {
404
</>
405
);
406
}
362
- React.startTransition(() => {
407
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
408
+ React.startTransition(() => {
409
+ root.render(<App />);
410
+ });
411
+ } else {
412
root.render(<App />);
364
- });
413
+ }
414
415
await waitFor(['A']);
416
await waitFor(['B']);
@@ -379,36 +428,62 @@ describe('ReactExpiration', () => {
428
429
jest.resetModules();
430
Scheduler = require('scheduler');
382
- const InternalTestUtils = require('internal-test-utils');
383
- waitFor = InternalTestUtils.waitFor;
384
- assertLog = InternalTestUtils.assertLog;
385
- unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
431
387
- // Before importing the renderer, advance the current time by a number
388
- // larger than the maximum allowed for bitwise operations.
389
- const maxSigned31BitInt = 1073741823;
390
- Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
391
-
392
- // Now import the renderer. On module initialization, it will read the
393
- // current time.
394
- ReactNoop = require('react-noop-renderer');
395
- React = require('react');
432
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
433
+ const InternalTestUtils = require('internal-test-utils');
434
+ waitFor = InternalTestUtils.waitFor;
435
+ assertLog = InternalTestUtils.assertLog;
436
+ unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
437
+
438
+ // Before importing the renderer, advance the current time by a number
439
+ // larger than the maximum allowed for bitwise operations.
440
+ const maxSigned31BitInt = 1073741823;
441
+ Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
442
+
443
+ // Now import the renderer. On module initialization, it will read the
444
+ // current time.
445
+ ReactNoop = require('react-noop-renderer');
446
+ React = require('react');
447
+
448
+ ReactNoop.render(<Text text="Step 1" />);
449
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
450
+ React.startTransition(() => {
451
+ ReactNoop.render(<Text text="Step 2" />);
452
+ });
453
+ await waitFor(['Step 1']);
454
+ } else {
455
+ ReactNoop.render('Hi');
456
+ }
457
397
- ReactNoop.render(<Text text="Step 1" />);
398
- React.startTransition(() => {
399
- ReactNoop.render(<Text text="Step 2" />);
400
- });
401
- await waitFor(['Step 1']);
458
+ // The update should not have expired yet.
459
+ await unstable_waitForExpired([]);
460
403
- // The update should not have expired yet.
404
- await unstable_waitForExpired([]);
461
+ expect(ReactNoop).toMatchRenderedOutput('Step 1');
462
406
- expect(ReactNoop).toMatchRenderedOutput('Step 1');
463
+ // Advance the time some more to expire the update.
464
+ Scheduler.unstable_advanceTime(10000);
465
+ await unstable_waitForExpired(['Step 2']);
466
+ expect(ReactNoop).toMatchRenderedOutput('Step 2');
467
+ } else {
468
+ // Before importing the renderer, advance the current time by a number
469
+ // larger than the maximum allowed for bitwise operations.
470
+ const maxSigned31BitInt = 1073741823;
471
+ Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
472
+ // Now import the renderer. On module initialization, it will read the
473
+ // current time.
474
+ ReactNoop = require('react-noop-renderer');
475
+ ReactNoop.render('Hi');
476
408
- // Advance the time some more to expire the update.
409
- Scheduler.unstable_advanceTime(10000);
410
- await unstable_waitForExpired(['Step 2']);
411
- expect(ReactNoop).toMatchRenderedOutput('Step 2');
477
+ // The update should not have expired yet.
478
+ flushNextRenderIfExpired();
479
+ await waitFor([]);
480
+ expect(ReactNoop).toMatchRenderedOutput(null);
481
+ // Advance the time some more to expire the update.
482
+ Scheduler.unstable_advanceTime(10000);
483
+ flushNextRenderIfExpired();
484
+ await waitFor([]);
485
+ expect(ReactNoop).toMatchRenderedOutput('Hi');
486
+ }
487
});
488
489
it('should measure callback timeout relative to current time, not start-up time', async () => {
@@ -419,9 +494,13 @@ describe('ReactExpiration', () => {
494
// Before scheduling an update, advance the current time.
495
Scheduler.unstable_advanceTime(10000);
496
422
- React.startTransition(() => {
497
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
498
+ React.startTransition(() => {
499
+ ReactNoop.render('Hi');
500
+ });
501
+ } else {
502
ReactNoop.render('Hi');
424
- });
503
+ }
504
await unstable_waitForExpired([]);
505
expect(ReactNoop).toMatchRenderedOutput(null);
506
@@ -462,9 +541,13 @@ describe('ReactExpiration', () => {
541
542
// First demonstrate what happens when there's no starvation
543
await act(async () => {
465
- React.startTransition(() => {
544
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
545
+ React.startTransition(() => {
546
+ updateNormalPri();
547
+ });
548
+ } else {
549
updateNormalPri();
467
- });
550
+ }
551
await waitFor(['Sync pri: 0']);
552
updateSyncPri();
553
assertLog(['Sync pri: 1', 'Normal pri: 0']);
@@ -482,9 +565,13 @@ describe('ReactExpiration', () => {
565
566
// Do the same thing, but starve the first update
567
await act(async () => {
485
- React.startTransition(() => {
568
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
569
+ React.startTransition(() => {
570
+ updateNormalPri();
571
+ });
572
+ } else {
573
updateNormalPri();
487
- });
574
+ }
575
await waitFor(['Sync pri: 1']);
576
577
// This time, a lot of time has elapsed since the normal pri update
@@ -645,9 +732,13 @@ describe('ReactExpiration', () => {
732
expect(root).toMatchRenderedOutput('A0BC');
733
734
await act(async () => {
648
- React.startTransition(() => {
735
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
736
+ React.startTransition(() => {
737
+ root.render(<App step={1} />);
738
+ });
739
+ } else {
740
root.render(<App step={1} />);
650
- });
741
+ }
742
await waitForAll(['Suspend! [A1]', 'Loading...']);
743
744
// Lots of time elapses before the promise resolves
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+6
-2
@@ -49,9 +49,13 @@ describe('ReactFlushSync', () => {
49
50
const root = ReactNoop.createRoot();
51
await act(async () => {
52
- React.startTransition(() => {
52
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
53
+ React.startTransition(() => {
54
+ root.render(<App />);
55
+ });
56
+ } else {
57
root.render(<App />);
54
- });
58
+ }
59
// This will yield right before the passive effect fires
60
await waitForPaint(['0, 0']);
61
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+77
-25
@@ -179,10 +179,15 @@ describe('ReactHooksWithNoopRenderer', () => {
179
180
// Schedule some updates
181
await act(async () => {
182
- React.startTransition(() => {
182
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
183
+ React.startTransition(() => {
184
+ counter.current.updateCount(1);
185
+ counter.current.updateCount(count => count + 10);
186
+ });
187
+ } else {
188
counter.current.updateCount(1);
189
counter.current.updateCount(count => count + 10);
185
- });
190
+ }
191
192
// Partially flush without committing
193
await waitFor(['Count: 11']);
@@ -687,16 +692,24 @@ describe('ReactHooksWithNoopRenderer', () => {
692
await waitForAll([0]);
693
expect(root).toMatchRenderedOutput(<span prop={0} />);
694
690
- React.startTransition(() => {
695
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
696
+ React.startTransition(() => {
697
+ root.render(<Foo signal={false} />);
698
+ });
699
+ } else {
700
root.render(<Foo signal={false} />);
692
- });
701
+ }
702
await waitForAll(['Suspend!']);
703
expect(root).toMatchRenderedOutput(<span prop={0} />);
704
705
// Rendering again should suspend again.
697
- React.startTransition(() => {
706
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
707
+ React.startTransition(() => {
708
+ root.render(<Foo signal={false} />);
709
+ });
710
+ } else {
711
root.render(<Foo signal={false} />);
699
- });
712
+ }
713
await waitForAll(['Suspend!']);
714
});
715
@@ -742,25 +755,38 @@ describe('ReactHooksWithNoopRenderer', () => {
755
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
756
757
await act(async () => {
745
- React.startTransition(() => {
758
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
759
+ React.startTransition(() => {
760
+ root.render(<Foo signal={false} />);
761
+ setLabel('B');
762
+ });
763
+ } else {
764
root.render(<Foo signal={false} />);
765
setLabel('B');
748
- });
766
+ }
767
768
await waitForAll(['Suspend!']);
769
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
770
771
// Rendering again should suspend again.
754
- React.startTransition(() => {
772
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
773
+ React.startTransition(() => {
774
+ root.render(<Foo signal={false} />);
775
+ });
776
+ } else {
777
root.render(<Foo signal={false} />);
756
- });
778
+ }
779
await waitForAll(['Suspend!']);
780
781
// Flip the signal back to "cancel" the update. However, the update to
782
// label should still proceed. It shouldn't have been dropped.
761
- React.startTransition(() => {
783
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
784
+ React.startTransition(() => {
785
+ root.render(<Foo signal={true} />);
786
+ });
787
+ } else {
788
root.render(<Foo signal={true} />);
763
- });
789
+ }
790
await waitForAll(['B:0']);
791
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
792
});
@@ -795,9 +821,13 @@ describe('ReactHooksWithNoopRenderer', () => {
821
ReactNoop.discreteUpdates(() => {
822
setRow(5);
823
});
798
- React.startTransition(() => {
824
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
825
+ React.startTransition(() => {
826
+ setRow(20);
827
+ });
828
+ } else {
829
setRow(20);
800
- });
830
+ }
831
});
832
assertLog(['Up', 'Down']);
833
expect(root).toMatchRenderedOutput(<span prop="Down" />);
@@ -1309,9 +1339,13 @@ describe('ReactHooksWithNoopRenderer', () => {
1339
]);
1340
1341
// Schedule another update for children, and partially process it.
1312
- React.startTransition(() => {
1342
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1343
+ React.startTransition(() => {
1344
+ setChildStates.forEach(setChildState => setChildState(2));
1345
+ });
1346
+ } else {
1347
setChildStates.forEach(setChildState => setChildState(2));
1314
- });
1348
+ }
1349
await waitFor(['Child one render']);
1350
1351
// Schedule unmount for the parent that unmounts children with pending update.
@@ -1585,21 +1619,39 @@ describe('ReactHooksWithNoopRenderer', () => {
1619
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
1620
1621
// Rendering again should flush the previous commit's effects
1588
- React.startTransition(() => {
1622
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1623
+ React.startTransition(() => {
1624
+ ReactNoop.render(<Counter count={1} />, () =>
1625
+ Scheduler.log('Sync effect'),
1626
+ );
1627
+ });
1628
+ } else {
1629
ReactNoop.render(<Counter count={1} />, () =>
1630
Scheduler.log('Sync effect'),
1631
);
1592
- });
1632
+ }
1633
1634
await waitFor(['Schedule update [0]', 'Count: 0']);
1635
1596
- expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
1597
- await waitFor([
1598
- 'Count: 0',
1599
- 'Sync effect',
1600
- 'Schedule update [1]',
1601
- 'Count: 1',
1602
- ]);
1636
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1637
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
1638
+ await waitFor([
1639
+ 'Count: 0',
1640
+ 'Sync effect',
1641
+ 'Schedule update [1]',
1642
+ 'Count: 1',
1643
+ ]);
1644
+ } else {
1645
+ expect(ReactNoop).toMatchRenderedOutput(
1646
+ <span prop="Count: (empty)" />,
1647
+ );
1648
+ await waitFor(['Sync effect']);
1649
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
1650
+
1651
+ ReactNoop.flushPassiveEffects();
1652
+ assertLog(['Schedule update [1]']);
1653
+ await waitForAll(['Count: 1']);
1654
+ }
1655
1656
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
1657
});
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+90
-20
@@ -75,9 +75,13 @@ describe('ReactIncremental', () => {
75
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
76
}
77
78
- React.startTransition(() => {
78
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
79
+ React.startTransition(() => {
80
+ ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
81
+ });
82
+ } else {
83
ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
80
- });
84
+ }
85
// Do one step of work.
86
await waitFor(['Foo']);
87
@@ -164,18 +168,26 @@ describe('ReactIncremental', () => {
168
ReactNoop.render(<Foo text="foo" />);
169
await waitForAll(['Foo', 'Bar', 'Bar']);
170
167
- React.startTransition(() => {
171
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
172
+ React.startTransition(() => {
173
+ ReactNoop.render(<Foo text="bar" />);
174
+ });
175
+ } else {
176
ReactNoop.render(<Foo text="bar" />);
169
- });
177
+ }
178
// Flush part of the work
179
await waitFor(['Foo', 'Bar']);
180
181
// This will abort the previous work and restart
182
ReactNoop.flushSync(() => ReactNoop.render(null));
183
176
- React.startTransition(() => {
184
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
185
+ React.startTransition(() => {
186
+ ReactNoop.render(<Foo text="baz" />);
187
+ });
188
+ } else {
189
ReactNoop.render(<Foo text="baz" />);
178
- });
190
+ }
191
192
// Flush part of the new work
193
await waitFor(['Foo', 'Bar']);
@@ -209,7 +221,17 @@ describe('ReactIncremental', () => {
221
ReactNoop.render(<Foo />);
222
await waitForAll([]);
223
212
- React.startTransition(() => {
224
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
225
+ React.startTransition(() => {
226
+ inst.setState(
227
+ () => {
228
+ Scheduler.log('setState1');
229
+ return {text: 'bar'};
230
+ },
231
+ () => Scheduler.log('callback1'),
232
+ );
233
+ });
234
+ } else {
235
inst.setState(
236
() => {
237
Scheduler.log('setState1');
@@ -217,14 +239,24 @@ describe('ReactIncremental', () => {
239
},
240
() => Scheduler.log('callback1'),
241
);
220
- });
242
+ }
243
244
// Flush part of the work
245
await waitFor(['setState1']);
246
247
// This will abort the previous work and restart
248
ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
227
- React.startTransition(() => {
249
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
250
+ React.startTransition(() => {
251
+ inst.setState(
252
+ () => {
253
+ Scheduler.log('setState2');
254
+ return {text2: 'baz'};
255
+ },
256
+ () => Scheduler.log('callback2'),
257
+ );
258
+ });
259
+ } else {
260
inst.setState(
261
() => {
262
Scheduler.log('setState2');
@@ -232,7 +264,7 @@ describe('ReactIncremental', () => {
264
},
265
() => Scheduler.log('callback2'),
266
);
235
- });
267
+ }
268
269
// Flush the rest of the work which now includes the low priority
270
await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']);
@@ -1793,7 +1825,18 @@ describe('ReactIncremental', () => {
1825
'ShowLocale {"locale":"de"}',
1826
'ShowBoth {"locale":"de"}',
1827
]);
1796
- React.startTransition(() => {
1828
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1829
+ React.startTransition(() => {
1830
+ ReactNoop.render(
1831
+ <Intl locale="sv">
1832
+ <ShowLocale />
1833
+ <div>
1834
+ <ShowBoth />
1835
+ </div>
1836
+ </Intl>,
1837
+ );
1838
+ });
1839
+ } else {
1840
ReactNoop.render(
1841
<Intl locale="sv">
1842
<ShowLocale />
@@ -1802,7 +1845,7 @@ describe('ReactIncremental', () => {
1845
</div>
1846
</Intl>,
1847
);
1805
- });
1848
+ }
1849
await waitFor(['Intl {}']);
1850
1851
ReactNoop.render(
@@ -1934,7 +1977,22 @@ describe('ReactIncremental', () => {
1977
}
1978
}
1979
1937
- React.startTransition(() => {
1980
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1981
+ React.startTransition(() => {
1982
+ ReactNoop.render(
1983
+ <Intl locale="fr">
1984
+ <ShowLocale />
1985
+ <LegacyHiddenDiv mode="hidden">
1986
+ <ShowLocale />
1987
+ <Intl locale="ru">
1988
+ <ShowLocale />
1989
+ </Intl>
1990
+ </LegacyHiddenDiv>
1991
+ <ShowLocale />
1992
+ </Intl>,
1993
+ );
1994
+ });
1995
+ } else {
1996
ReactNoop.render(
1997
<Intl locale="fr">
1998
<ShowLocale />
@@ -1947,7 +2005,7 @@ describe('ReactIncremental', () => {
2005
<ShowLocale />
2006
</Intl>,
2007
);
1950
- });
2008
+ }
2009
await waitFor([
2010
'Intl {}',
2011
'ShowLocale {"locale":"fr"}',
@@ -2624,9 +2682,13 @@ describe('ReactIncremental', () => {
2682
return null;
2683
}
2684
2627
- React.startTransition(() => {
2685
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2686
+ React.startTransition(() => {
2687
+ ReactNoop.render(<Parent step={1} />);
2688
+ });
2689
+ } else {
2690
ReactNoop.render(<Parent step={1} />);
2629
- });
2691
+ }
2692
await waitFor(['Parent: 1']);
2693
2694
// Interrupt at same priority
@@ -2646,9 +2708,13 @@ describe('ReactIncremental', () => {
2708
return null;
2709
}
2710
2649
- React.startTransition(() => {
2711
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2712
+ React.startTransition(() => {
2713
+ ReactNoop.render(<Parent step={1} />);
2714
+ });
2715
+ } else {
2716
ReactNoop.render(<Parent step={1} />);
2651
- });
2717
+ }
2718
await waitFor(['Parent: 1']);
2719
2720
// Interrupt at lower priority
@@ -2669,9 +2735,13 @@ describe('ReactIncremental', () => {
2735
return null;
2736
}
2737
2672
- React.startTransition(() => {
2738
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2739
+ React.startTransition(() => {
2740
+ ReactNoop.render(<Parent step={1} />);
2741
+ });
2742
+ } else {
2743
ReactNoop.render(<Parent step={1} />);
2674
- });
2744
+ }
2745
await waitFor(['Parent: 1']);
2746
2747
// Interrupt at higher priority
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+78
-14
@@ -97,7 +97,25 @@ describe('ReactIncrementalErrorHandling', () => {
97
throw new Error('oops!');
98
}
99
100
- React.startTransition(() => {
100
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
101
+ React.startTransition(() => {
102
+ ReactNoop.render(
103
+ <>
104
+ <ErrorBoundary>
105
+ <Indirection>
106
+ <Indirection>
107
+ <Indirection>
108
+ <BadRender />
109
+ </Indirection>
110
+ </Indirection>
111
+ </Indirection>
112
+ </ErrorBoundary>
113
+ <Indirection />
114
+ <Indirection />
115
+ </>,
116
+ );
117
+ });
118
+ } else {
119
ReactNoop.render(
120
<>
121
<ErrorBoundary>
@@ -113,7 +131,7 @@ describe('ReactIncrementalErrorHandling', () => {
131
<Indirection />
132
</>,
133
);
116
- });
134
+ }
135
136
// Start rendering asynchronously
137
await waitFor([
@@ -196,7 +214,25 @@ describe('ReactIncrementalErrorHandling', () => {
214
throw new Error('oops!');
215
}
216
199
- React.startTransition(() => {
217
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
218
+ React.startTransition(() => {
219
+ ReactNoop.render(
220
+ <>
221
+ <ErrorBoundary>
222
+ <Indirection>
223
+ <Indirection>
224
+ <Indirection>
225
+ <BadRender />
226
+ </Indirection>
227
+ </Indirection>
228
+ </Indirection>
229
+ </ErrorBoundary>
230
+ <Indirection />
231
+ <Indirection />
232
+ </>,
233
+ );
234
+ });
235
+ } else {
236
ReactNoop.render(
237
<>
238
<ErrorBoundary>
@@ -212,7 +248,7 @@ describe('ReactIncrementalErrorHandling', () => {
248
<Indirection />
249
</>,
250
);
215
- });
251
+ }
252
253
// Start rendering asynchronously
254
await waitFor([
@@ -380,9 +416,13 @@ describe('ReactIncrementalErrorHandling', () => {
416
);
417
}
418
383
- React.startTransition(() => {
419
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
420
+ React.startTransition(() => {
421
+ ReactNoop.render(<Parent />, () => Scheduler.log('commit'));
422
+ });
423
+ } else {
424
ReactNoop.render(<Parent />, () => Scheduler.log('commit'));
385
- });
425
+ }
426
427
// Render the bad component asynchronously
428
await waitFor(['Parent', 'BadRender']);
@@ -418,9 +458,13 @@ describe('ReactIncrementalErrorHandling', () => {
458
);
459
}
460
421
- React.startTransition(() => {
461
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
462
+ React.startTransition(() => {
463
+ ReactNoop.render(<App />);
464
+ });
465
+ } else {
466
ReactNoop.render(<App />);
423
- });
467
+ }
468
469
// Render part of the tree
470
await waitFor(['A', 'B']);
@@ -551,13 +595,21 @@ describe('ReactIncrementalErrorHandling', () => {
595
throw new Error('Hello');
596
}
597
554
- React.startTransition(() => {
598
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
599
+ React.startTransition(() => {
600
+ ReactNoop.render(
601
+ <ErrorBoundary>
602
+ <BrokenRender />
603
+ </ErrorBoundary>,
604
+ );
605
+ });
606
+ } else {
607
ReactNoop.render(
608
<ErrorBoundary>
609
<BrokenRender />
610
</ErrorBoundary>,
611
);
560
- });
612
+ }
613
614
await waitFor(['ErrorBoundary render success']);
615
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -731,13 +783,21 @@ describe('ReactIncrementalErrorHandling', () => {
783
throw new Error('Hello');
784
}
785
734
- React.startTransition(() => {
786
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
787
+ React.startTransition(() => {
788
+ ReactNoop.render(
789
+ <RethrowErrorBoundary>
790
+ <BrokenRender />
791
+ </RethrowErrorBoundary>,
792
+ );
793
+ });
794
+ } else {
795
ReactNoop.render(
796
<RethrowErrorBoundary>
797
<BrokenRender />
798
</RethrowErrorBoundary>,
799
);
740
- });
800
+ }
801
802
await waitFor(['RethrowErrorBoundary render']);
803
@@ -1796,9 +1856,13 @@ describe('ReactIncrementalErrorHandling', () => {
1856
}
1857
1858
await act(async () => {
1799
- React.startTransition(() => {
1859
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1860
+ React.startTransition(() => {
1861
+ root.render(<Oops />);
1862
+ });
1863
+ } else {
1864
root.render(<Oops />);
1801
- });
1865
+ }
1866
1867
// Render past the component that throws, then yield.
1868
await waitFor(['Oops']);
packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
+30
-10
@@ -65,9 +65,13 @@ describe('ReactIncrementalReflection', () => {
65
return <Component />;
66
}
67
68
- React.startTransition(() => {
68
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
69
+ React.startTransition(() => {
70
+ ReactNoop.render(<Foo />);
71
+ });
72
+ } else {
73
ReactNoop.render(<Foo />);
70
- });
74
+ }
75
76
// Render part way through but don't yet commit the updates.
77
await waitFor(['componentWillMount: false']);
@@ -113,9 +117,13 @@ describe('ReactIncrementalReflection', () => {
117
118
expect(instances[0]._isMounted()).toBe(true);
119
116
- React.startTransition(() => {
120
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
121
+ React.startTransition(() => {
122
+ ReactNoop.render(<Foo mount={false} />);
123
+ });
124
+ } else {
125
ReactNoop.render(<Foo mount={false} />);
118
- });
126
+ }
127
// Render part way through but don't yet commit the updates so it is not
128
// fully unmounted yet.
129
await waitFor(['Other']);
@@ -183,9 +191,13 @@ describe('ReactIncrementalReflection', () => {
191
return [<Component key="a" step={props.step} />, <Sibling key="b" />];
192
}
193
186
- React.startTransition(() => {
194
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
195
+ React.startTransition(() => {
196
+ ReactNoop.render(<Foo step={0} />);
197
+ });
198
+ } else {
199
ReactNoop.render(<Foo step={0} />);
188
- });
200
+ }
201
// Flush past Component but don't complete rendering everything yet.
202
await waitFor([['componentWillMount', null], 'render', 'render sibling']);
203
@@ -215,9 +227,13 @@ describe('ReactIncrementalReflection', () => {
227
228
// The next step will render a new host node but won't get committed yet.
229
// We expect this to mutate the original Fiber.
218
- React.startTransition(() => {
230
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
231
+ React.startTransition(() => {
232
+ ReactNoop.render(<Foo step={2} />);
233
+ });
234
+ } else {
235
ReactNoop.render(<Foo step={2} />);
220
- });
236
+ }
237
await waitFor([
238
['componentWillUpdate', hostSpan],
239
'render',
@@ -238,9 +254,13 @@ describe('ReactIncrementalReflection', () => {
254
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
255
256
// Render to null but don't commit it yet.
241
- React.startTransition(() => {
257
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
258
+ React.startTransition(() => {
259
+ ReactNoop.render(<Foo step={3} />);
260
+ });
261
+ } else {
262
ReactNoop.render(<Foo step={3} />);
243
- });
263
+ }
264
await waitFor([
265
['componentWillUpdate', hostDiv],
266
'render',
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+31
-10
@@ -115,10 +115,15 @@ describe('ReactIncrementalScheduling', () => {
115
116
// Schedule deferred work in the reverse order
117
await act(async () => {
118
- React.startTransition(() => {
118
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
119
+ React.startTransition(() => {
120
+ ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
121
+ ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
122
+ });
123
+ } else {
124
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
125
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
121
- });
126
+ }
127
// Ensure it starts in the order it was scheduled
128
await waitFor(['c:2']);
129
@@ -127,9 +132,13 @@ describe('ReactIncrementalScheduling', () => {
132
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
133
// Schedule last bit of work, it will get processed the last
134
130
- React.startTransition(() => {
135
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
136
+ React.startTransition(() => {
137
+ ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
138
+ });
139
+ } else {
140
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
132
- });
141
+ }
142
143
// Keep performing work in the order it was scheduled
144
await waitFor(['b:2']);
@@ -180,9 +189,13 @@ describe('ReactIncrementalScheduling', () => {
189
}
190
}
191
183
- React.startTransition(() => {
192
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
193
+ React.startTransition(() => {
194
+ ReactNoop.render(<Foo />);
195
+ });
196
+ } else {
197
ReactNoop.render(<Foo />);
185
- });
198
+ }
199
// Render without committing
200
await waitFor(['render: 0']);
201
@@ -196,9 +209,13 @@ describe('ReactIncrementalScheduling', () => {
209
'componentDidUpdate: 1',
210
]);
211
199
- React.startTransition(() => {
212
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
213
+ React.startTransition(() => {
214
+ instance.setState({tick: 2});
215
+ });
216
+ } else {
217
instance.setState({tick: 2});
201
- });
218
+ }
219
await waitFor(['render: 2']);
220
expect(ReactNoop.flushNextYield()).toEqual([
221
'componentDidUpdate: 2',
@@ -299,9 +316,13 @@ describe('ReactIncrementalScheduling', () => {
316
return <span prop={this.state.step} />;
317
}
318
}
302
- React.startTransition(() => {
319
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
320
+ React.startTransition(() => {
321
+ ReactNoop.render(<Foo />);
322
+ });
323
+ } else {
324
ReactNoop.render(<Foo />);
304
- });
325
+ }
326
327
// This should be just enough to complete all the work, but not enough to
328
// commit it.
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+24
-8
@@ -464,9 +464,13 @@ describe('ReactIncrementalSideEffects', () => {
464
</div>,
465
);
466
467
- React.startTransition(() => {
467
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
468
+ React.startTransition(() => {
469
+ ReactNoop.render(<Foo text="World" />);
470
+ });
471
+ } else {
472
ReactNoop.render(<Foo text="World" />);
469
- });
473
+ }
474
475
// Flush some of the work without committing
476
await waitFor(['Foo', 'Bar']);
@@ -699,9 +703,13 @@ describe('ReactIncrementalSideEffects', () => {
703
Scheduler.log('Foo ' + props.step);
704
return <span prop={props.step} />;
705
}
702
- React.startTransition(() => {
706
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
707
+ React.startTransition(() => {
708
+ ReactNoop.render(<Foo step={1} />);
709
+ });
710
+ } else {
711
ReactNoop.render(<Foo step={1} />);
704
- });
712
+ }
713
// This should be just enough to complete the tree without committing it
714
await waitFor(['Foo 1']);
715
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
@@ -710,18 +718,26 @@ describe('ReactIncrementalSideEffects', () => {
718
await waitForPaint([]);
719
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
720
713
- React.startTransition(() => {
721
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
722
+ React.startTransition(() => {
723
+ ReactNoop.render(<Foo step={2} />);
724
+ });
725
+ } else {
726
ReactNoop.render(<Foo step={2} />);
715
- });
727
+ }
728
// This should be just enough to complete the tree without committing it
729
await waitFor(['Foo 2']);
730
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
731
// This time, before we commit the tree, we update the root component with
732
// new props
733
722
- React.startTransition(() => {
734
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
735
+ React.startTransition(() => {
736
+ ReactNoop.render(<Foo step={3} />);
737
+ });
738
+ } else {
739
ReactNoop.render(<Foo step={3} />);
724
- });
740
+ }
741
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
742
// Now let's commit. We already had a commit that was pending, which will
743
// render 2.
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+107
-23
@@ -156,11 +156,21 @@ describe('ReactIncrementalUpdates', () => {
156
}
157
158
// Schedule some async updates
159
- React.startTransition(() => {
159
+ if (
160
+ gate(
161
+ flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
162
+ )
163
+ ) {
164
+ React.startTransition(() => {
165
+ instance.setState(createUpdate('a'));
166
+ instance.setState(createUpdate('b'));
167
+ instance.setState(createUpdate('c'));
168
+ });
169
+ } else {
170
instance.setState(createUpdate('a'));
171
instance.setState(createUpdate('b'));
172
instance.setState(createUpdate('c'));
163
- });
173
+ }
174
175
// Begin the updates but don't flush them yet
176
await waitFor(['a', 'b', 'c']);
@@ -177,7 +187,11 @@ describe('ReactIncrementalUpdates', () => {
187
});
188
189
// The sync updates should have flushed, but not the async ones.
180
- if (gate(flags => flags.enableUnifiedSyncLane)) {
190
+ if (
191
+ gate(
192
+ flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
193
+ )
194
+ ) {
195
assertLog(['d', 'e', 'f']);
196
expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />);
197
} else {
@@ -189,7 +203,11 @@ describe('ReactIncrementalUpdates', () => {
203
// Now flush the remaining work. Even though e and f were already processed,
204
// they should be processed again, to ensure that the terminal state
205
// is deterministic.
192
- if (gate(flags => !flags.enableUnifiedSyncLane)) {
206
+ if (
207
+ gate(
208
+ flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
209
+ )
210
+ ) {
211
await waitForAll([
212
// Since 'g' is in a transition, we'll process 'd' separately first.
213
// That causes us to process 'd' with 'e' and 'f' rebased.
@@ -243,11 +261,21 @@ describe('ReactIncrementalUpdates', () => {
261
}
262
263
// Schedule some async updates
246
- React.startTransition(() => {
264
+ if (
265
+ gate(
266
+ flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
267
+ )
268
+ ) {
269
+ React.startTransition(() => {
270
+ instance.setState(createUpdate('a'));
271
+ instance.setState(createUpdate('b'));
272
+ instance.setState(createUpdate('c'));
273
+ });
274
+ } else {
275
instance.setState(createUpdate('a'));
276
instance.setState(createUpdate('b'));
277
instance.setState(createUpdate('c'));
250
- });
278
+ }
279
280
// Begin the updates but don't flush them yet
281
await waitFor(['a', 'b', 'c']);
@@ -267,7 +295,11 @@ describe('ReactIncrementalUpdates', () => {
295
});
296
297
// The sync updates should have flushed, but not the async ones.
270
- if (gate(flags => flags.enableUnifiedSyncLane)) {
298
+ if (
299
+ gate(
300
+ flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
301
+ )
302
+ ) {
303
assertLog(['d', 'e', 'f']);
304
} else {
305
// Update d was dropped and replaced by e.
@@ -278,7 +310,11 @@ describe('ReactIncrementalUpdates', () => {
310
// Now flush the remaining work. Even though e and f were already processed,
311
// they should be processed again, to ensure that the terminal state
312
// is deterministic.
281
- if (gate(flags => !flags.enableUnifiedSyncLane)) {
313
+ if (
314
+ gate(
315
+ flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
316
+ )
317
+ ) {
318
await waitForAll([
319
// Since 'g' is in a transition, we'll process 'd' separately first.
320
// That causes us to process 'd' with 'e' and 'f' rebased.
@@ -507,9 +543,13 @@ describe('ReactIncrementalUpdates', () => {
543
}
544
545
await act(async () => {
510
- React.startTransition(() => {
546
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
547
+ React.startTransition(() => {
548
+ ReactNoop.render(<App />);
549
+ });
550
+ } else {
551
ReactNoop.render(<App />);
512
- });
552
+ }
553
assertLog([]);
554
await waitForAll([
555
'Render: 0',
@@ -520,9 +560,13 @@ describe('ReactIncrementalUpdates', () => {
560
]);
561
562
Scheduler.unstable_advanceTime(10000);
523
- React.startTransition(() => {
563
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
564
+ React.startTransition(() => {
565
+ setCount(2);
566
+ });
567
+ } else {
568
setCount(2);
525
- });
569
+ }
570
// The transition should not have expired, so we should be able to
571
// partially render it.
572
await waitFor(['Render: 2']);
@@ -539,7 +583,18 @@ describe('ReactIncrementalUpdates', () => {
583
584
Scheduler.unstable_advanceTime(10000);
585
542
- React.startTransition(() => {
586
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
587
+ React.startTransition(() => {
588
+ ReactNoop.render(
589
+ <>
590
+ <Text text="A" />
591
+ <Text text="B" />
592
+ <Text text="C" />
593
+ <Text text="D" />
594
+ </>,
595
+ );
596
+ });
597
+ } else {
598
ReactNoop.render(
599
<>
600
<Text text="A" />
@@ -548,7 +603,7 @@ describe('ReactIncrementalUpdates', () => {
603
<Text text="D" />
604
</>,
605
);
551
- });
606
+ }
607
// The transition should not have expired, so we should be able to
608
// partially render it.
609
await waitFor(['A']);
@@ -557,7 +612,18 @@ describe('ReactIncrementalUpdates', () => {
612
});
613
614
it('regression: does not expire soon due to previous expired work', async () => {
560
- React.startTransition(() => {
615
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
616
+ React.startTransition(() => {
617
+ ReactNoop.render(
618
+ <>
619
+ <Text text="A" />
620
+ <Text text="B" />
621
+ <Text text="C" />
622
+ <Text text="D" />
623
+ </>,
624
+ );
625
+ });
626
+ } else {
627
ReactNoop.render(
628
<>
629
<Text text="A" />
@@ -566,9 +632,8 @@ describe('ReactIncrementalUpdates', () => {
632
<Text text="D" />
633
</>,
634
);
569
- });
635
+ }
636
await waitFor(['A']);
571
-
637
// This will expire the rest of the update
638
Scheduler.unstable_advanceTime(10000);
639
await waitFor(['B'], {
@@ -578,7 +643,18 @@ describe('ReactIncrementalUpdates', () => {
643
Scheduler.unstable_advanceTime(10000);
644
645
// Now do another transition. This one should not expire.
581
- React.startTransition(() => {
646
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
647
+ React.startTransition(() => {
648
+ ReactNoop.render(
649
+ <>
650
+ <Text text="A" />
651
+ <Text text="B" />
652
+ <Text text="C" />
653
+ <Text text="D" />
654
+ </>,
655
+ );
656
+ });
657
+ } else {
658
ReactNoop.render(
659
<>
660
<Text text="A" />
@@ -587,7 +663,7 @@ describe('ReactIncrementalUpdates', () => {
663
<Text text="D" />
664
</>,
665
);
590
- });
666
+ }
667
// The transition should not have expired, so we should be able to
668
// partially render it.
669
await waitFor(['A']);
@@ -627,9 +703,13 @@ describe('ReactIncrementalUpdates', () => {
703
expect(root).toMatchRenderedOutput(null);
704
705
await act(() => {
630
- React.startTransition(() => {
706
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
707
+ React.startTransition(() => {
708
+ pushToLog('A');
709
+ });
710
+ } else {
711
pushToLog('A');
632
- });
712
+ }
713
714
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
715
pushToLog('B'),
@@ -688,9 +768,13 @@ describe('ReactIncrementalUpdates', () => {
768
expect(root).toMatchRenderedOutput(null);
769
770
await act(() => {
691
- React.startTransition(() => {
771
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
772
+ React.startTransition(() => {
773
+ pushToLog('A');
774
+ });
775
+ } else {
776
pushToLog('A');
693
- });
777
+ }
778
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
779
pushToLog('B'),
780
);
packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js
+66
-3
@@ -65,15 +65,78 @@ describe('ReactInterleavedUpdates', () => {
65
expect(root).toMatchRenderedOutput('000');
66
67
await act(async () => {
68
- React.startTransition(() => {
68
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
69
+ React.startTransition(() => {
70
+ updateChildren(1);
71
+ });
72
+ } else {
73
updateChildren(1);
70
- });
74
+ }
75
+ // Partially render the children. Only the first one.
76
+ await waitFor([1]);
77
+
78
+ // In an interleaved event, schedule an update on each of the children.
79
+ // Including the two that haven't rendered yet.
80
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
81
+ React.startTransition(() => {
82
+ updateChildren(2);
83
+ });
84
+ } else {
85
+ updateChildren(2);
86
+ }
87
+
88
+ // We should continue rendering without including the interleaved updates.
89
+ await waitForPaint([1, 1]);
90
+ expect(root).toMatchRenderedOutput('111');
91
+ });
92
+ // The interleaved updates flush in a separate render.
93
+ assertLog([2, 2, 2]);
94
+ expect(root).toMatchRenderedOutput('222');
95
+ });
96
+
97
+ // @gate !enableSyncDefaultUpdates
98
+ test('low priority update during an interleaved event is not processed during the current render', async () => {
99
+ // Same as previous test, but the interleaved update is lower priority than
100
+ // the in-progress render.
101
+ const updaters = [];
102
+
103
+ function Child() {
104
+ const [state, setState] = useState(0);
105
+ useEffect(() => {
106
+ updaters.push(setState);
107
+ }, []);
108
+ return <Text text={state} />;
109
+ }
110
+
111
+ function updateChildren(value) {
112
+ for (let i = 0; i < updaters.length; i++) {
113
+ const setState = updaters[i];
114
+ setState(value);
115
+ }
116
+ }
117
+
118
+ const root = ReactNoop.createRoot();
119
+
120
+ await act(async () => {
121
+ root.render(
122
+ <>
123
+ <Child />
124
+ <Child />
125
+ <Child />
126
+ </>,
127
+ );
128
+ });
129
+ assertLog([0, 0, 0]);
130
+ expect(root).toMatchRenderedOutput('000');
131
+
132
+ await act(async () => {
133
+ updateChildren(1);
134
// Partially render the children. Only the first one.
135
await waitFor([1]);
136
137
// In an interleaved event, schedule an update on each of the children.
138
// Including the two that haven't rendered yet.
76
- React.startTransition(() => {
139
+ startTransition(() => {
140
updateChildren(2);
141
});
142
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+6
-2
@@ -1559,9 +1559,13 @@ describe('ReactLazy', () => {
1559
expect(root).toMatchRenderedOutput('AB');
1560
1561
// Swap the position of A and B
1562
- React.startTransition(() => {
1562
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1563
+ React.startTransition(() => {
1564
+ root.update(<Parent swap={true} />);
1565
+ });
1566
+ } else {
1567
root.update(<Parent swap={true} />);
1564
- });
1568
+ }
1569
await waitForAll(['Init B2', 'Loading...']);
1570
await resolveFakeImport(ChildB2);
1571
// We need to flush to trigger the second one to load.
packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+12
-4
@@ -885,9 +885,13 @@ describe('ReactNewContext', () => {
885
);
886
}
887
888
- React.startTransition(() => {
888
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
889
+ React.startTransition(() => {
890
+ ReactNoop.render(<App value={1} />);
891
+ });
892
+ } else {
893
ReactNoop.render(<App value={1} />);
890
- });
894
+ }
895
// Render past the Provider, but don't commit yet
896
await waitFor(['Foo']);
897
@@ -930,9 +934,13 @@ describe('ReactNewContext', () => {
934
);
935
}
936
933
- React.startTransition(() => {
937
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
938
+ React.startTransition(() => {
939
+ ReactNoop.render(<App value={1} />);
940
+ });
941
+ } else {
942
ReactNoop.render(<App value={1} />);
935
- });
943
+ }
944
await waitForAll(['Foo', 'Foo']);
945
946
// Get a new copy of ReactNoop
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+6
-2
@@ -109,9 +109,13 @@ describe('ReactSchedulerIntegration', () => {
109
scheduleCallback(NormalPriority, () => Scheduler.log('C'));
110
111
// Schedule a React render. React will request a paint after committing it.
112
- React.startTransition(() => {
112
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
113
+ React.startTransition(() => {
114
+ root.render('Update');
115
+ });
116
+ } else {
117
root.render('Update');
114
- });
118
+ }
119
120
// Perform just a little bit of work. By now, the React task will have
121
// already been scheduled, behind A, B, and C.
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+91
-4
@@ -125,9 +125,13 @@ describe('ReactSuspense', () => {
125
126
// Navigate the shell to now render the child content.
127
// This should suspend.
128
- React.startTransition(() => {
128
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
129
+ React.startTransition(() => {
130
+ root.update(<Foo renderBar={true} />);
131
+ });
132
+ } else {
133
root.update(<Foo renderBar={true} />);
130
- });
134
+ }
135
136
await waitForAll([
137
'Foo',
@@ -224,7 +228,19 @@ describe('ReactSuspense', () => {
228
expect(root).toMatchRenderedOutput('Initial');
229
230
// The update will suspend.
227
- React.startTransition(() => {
231
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
232
+ React.startTransition(() => {
233
+ root.update(
234
+ <>
235
+ <Suspense fallback={<Text text="Loading..." />}>
236
+ <Async />
237
+ </Suspense>
238
+ <Text text="After Suspense" />
239
+ <Text text="Sibling" />
240
+ </>,
241
+ );
242
+ });
243
+ } else {
244
root.update(
245
<>
246
<Suspense fallback={<Text text="Loading..." />}>
@@ -234,7 +250,8 @@ describe('ReactSuspense', () => {
250
<Text text="Sibling" />
251
</>,
252
);
237
- });
253
+ }
254
+
255
// Yield past the Suspense boundary but don't complete the last sibling.
256
await waitFor(['Suspend!', 'Loading...', 'After Suspense']);
257
@@ -329,6 +346,76 @@ describe('ReactSuspense', () => {
346
expect(root).toMatchRenderedOutput('AB');
347
});
348
349
+ // @gate !enableSyncDefaultUpdates
350
+ it(
351
+ 'interrupts current render when something suspends with a ' +
352
+ "delay and we've already skipped over a lower priority update in " +
353
+ 'a parent',
354
+ async () => {
355
+ function interrupt() {
356
+ // React has a heuristic to batch all updates that occur within the same
357
+ // event. This is a trick to circumvent that heuristic.
358
+ ReactTestRenderer.create('whatever');
359
+ }
360
+
361
+ function App({shouldSuspend, step}) {
362
+ return (
363
+ <>
364
+ <Text text={`A${step}`} />
365
+ <Suspense fallback={<Text text="Loading..." />}>
366
+ {shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
367
+ </Suspense>
368
+ <Text text={`B${step}`} />
369
+ <Text text={`C${step}`} />
370
+ </>
371
+ );
372
+ }
373
+
374
+ const root = ReactTestRenderer.create(null, {
375
+ unstable_isConcurrent: true,
376
+ });
377
+
378
+ root.update(<App shouldSuspend={false} step={0} />);
379
+ await waitForAll(['A0', 'B0', 'C0']);
380
+ expect(root).toMatchRenderedOutput('A0B0C0');
381
+
382
+ // This update will suspend.
383
+ root.update(<App shouldSuspend={true} step={1} />);
384
+
385
+ // Do a bit of work
386
+ await waitFor(['A1']);
387
+
388
+ // Schedule another update. This will have lower priority because it's
389
+ // a transition.
390
+ React.startTransition(() => {
391
+ root.update(<App shouldSuspend={false} step={2} />);
392
+ });
393
+
394
+ // Interrupt to trigger a restart.
395
+ interrupt();
396
+
397
+ await waitFor([
398
+ // Should have restarted the first update, because of the interruption
399
+ 'A1',
400
+ 'Suspend! [Async]',
401
+ 'Loading...',
402
+ 'B1',
403
+ ]);
404
+
405
+ // Should not have committed loading state
406
+ expect(root).toMatchRenderedOutput('A0B0C0');
407
+
408
+ // After suspending, should abort the first update and switch to the
409
+ // second update. So, C1 should not appear in the log.
410
+ // TODO: This should work even if React does not yield to the main
411
+ // thread. Should use same mechanism as selective hydration to interrupt
412
+ // the render before the end of the current slice of work.
413
+ await waitForAll(['A2', 'B2', 'C2']);
414
+
415
+ expect(root).toMatchRenderedOutput('A2B2C2');
416
+ },
417
+ );
418
+
419
it('mounts a lazy class component in non-concurrent mode', async () => {
420
class Class extends React.Component {
421
componentDidMount() {
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+8
-8
@@ -576,7 +576,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
576
]);
577
});
578
579
- // @gate enableLegacyCache
579
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
580
it('should be destroyed and recreated for function components', async () => {
581
function App({children = null}) {
582
Scheduler.log('App render');
@@ -711,7 +711,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
711
]);
712
});
713
714
- // @gate enableLegacyCache
714
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
715
it('should be destroyed and recreated for class components', async () => {
716
class ClassText extends React.Component {
717
componentDidMount() {
@@ -860,7 +860,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
860
]);
861
});
862
863
- // @gate enableLegacyCache
863
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
864
it('should be destroyed and recreated when nested below host components', async () => {
865
function App({children = null}) {
866
Scheduler.log('App render');
@@ -979,7 +979,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
979
]);
980
});
981
982
- // @gate enableLegacyCache
982
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
983
it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
984
const MemoizedText = React.memo(Text, () => true);
985
@@ -1448,7 +1448,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1448
);
1449
});
1450
1451
- // @gate enableLegacyCache
1451
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
1452
it('should be cleaned up inside of a fallback that suspends', async () => {
1453
function App({fallbackChildren = null, outerChildren = null}) {
1454
return (
@@ -1724,7 +1724,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1724
);
1725
});
1726
1727
- // @gate enableLegacyCache
1727
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
1728
it('should be cleaned up deeper inside of a subtree that suspends', async () => {
1729
function ConditionalSuspense({shouldSuspend}) {
1730
if (shouldSuspend) {
@@ -2305,7 +2305,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2305
});
2306
});
2307
2308
- // @gate enableLegacyCache
2308
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
2309
it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
2310
class ClassText extends React.Component {
2311
componentDidMount() {
@@ -2448,7 +2448,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2448
]);
2449
});
2450
2451
- // @gate enableLegacyCache
2451
+ // @gate enableLegacyCache && enableSyncDefaultUpdates
2452
it('should be only destroy layout effects once if a component suspends multiple times', async () => {
2453
class ClassText extends React.Component {
2454
componentDidMount() {
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+34
-10
@@ -1366,9 +1366,13 @@ describe('ReactSuspenseList', () => {
1366
}
1367
1368
// This render is only CPU bound. Nothing suspends.
1369
- React.startTransition(() => {
1369
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1370
+ React.startTransition(() => {
1371
+ ReactNoop.render(<Foo />);
1372
+ });
1373
+ } else {
1374
ReactNoop.render(<Foo />);
1371
- });
1375
+ }
1376
1377
await waitFor(['A']);
1378
@@ -1550,9 +1554,13 @@ describe('ReactSuspenseList', () => {
1554
}
1555
1556
// This render is only CPU bound. Nothing suspends.
1553
- React.startTransition(() => {
1557
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1558
+ React.startTransition(() => {
1559
+ ReactNoop.render(<Foo />);
1560
+ });
1561
+ } else {
1562
ReactNoop.render(<Foo />);
1555
- });
1563
+ }
1564
1565
await waitFor(['A']);
1566
@@ -2517,9 +2525,15 @@ describe('ReactSuspenseList', () => {
2525
expect(ReactNoop).toMatchRenderedOutput(null);
2526
2527
await act(async () => {
2520
- React.startTransition(() => {
2528
+ // Add a few items at the end.
2529
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2530
+ React.startTransition(() => {
2531
+ updateLowPri(true);
2532
+ });
2533
+ } else {
2534
updateLowPri(true);
2522
- });
2535
+ }
2536
+
2537
// Flush partially through.
2538
await waitFor(['B', 'C']);
2539
@@ -2655,9 +2669,14 @@ describe('ReactSuspenseList', () => {
2669
);
2670
}
2671
2658
- React.startTransition(() => {
2672
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2673
+ React.startTransition(() => {
2674
+ ReactNoop.render(<App />);
2675
+ });
2676
+ } else {
2677
ReactNoop.render(<App />);
2660
- });
2678
+ }
2679
+
2680
await waitFor(['App', 'First Pass A', 'Mount A', 'A']);
2681
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
2682
@@ -2718,9 +2737,14 @@ describe('ReactSuspenseList', () => {
2737
);
2738
}
2739
2721
- React.startTransition(() => {
2740
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2741
+ React.startTransition(() => {
2742
+ ReactNoop.render(<App />);
2743
+ });
2744
+ } else {
2745
ReactNoop.render(<App />);
2723
- });
2746
+ }
2747
+
2748
await waitFor([
2749
'App',
2750
'First Pass A',
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+266
-44
@@ -216,9 +216,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
216
);
217
}
218
219
- React.startTransition(() => {
219
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
220
+ React.startTransition(() => {
221
+ ReactNoop.render(<Foo />);
222
+ });
223
+ } else {
224
ReactNoop.render(<Foo />);
221
- });
225
+ }
226
await waitFor([
227
'Foo',
228
'Bar',
@@ -285,9 +289,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
289
await waitForAll(['Foo']);
290
291
// The update will suspend.
288
- React.startTransition(() => {
292
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
293
+ React.startTransition(() => {
294
+ ReactNoop.render(<Foo renderBar={true} />);
295
+ });
296
+ } else {
297
ReactNoop.render(<Foo renderBar={true} />);
290
- });
298
+ }
299
await waitForAll([
300
'Foo',
301
'Bar',
@@ -367,7 +375,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
375
// A shell is needed. The update cause it to suspend.
376
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
377
await waitForAll([]);
370
- React.startTransition(() => {
378
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
379
+ React.startTransition(() => {
380
+ ReactNoop.render(
381
+ <Suspense fallback={<Text text="Loading..." />}>
382
+ <Text text="A" />
383
+ <AsyncText text="B" />
384
+ <Text text="C" />
385
+ <Text text="D" />
386
+ </Suspense>,
387
+ );
388
+ });
389
+ } else {
390
ReactNoop.render(
391
<Suspense fallback={<Text text="Loading..." />}>
392
<Text text="A" />
@@ -376,7 +395,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
395
<Text text="D" />
396
</Suspense>,
397
);
379
- });
398
+ }
399
// B suspends. Render a fallback
400
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
401
// Did not commit yet.
@@ -434,9 +453,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
453
await waitForAll([]);
454
expect(ReactNoop).toMatchRenderedOutput(null);
455
437
- React.startTransition(() => {
456
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
457
+ React.startTransition(() => {
458
+ ReactNoop.render(<App renderContent={true} />);
459
+ });
460
+ } else {
461
ReactNoop.render(<App renderContent={true} />);
439
- });
462
+ }
463
await waitForAll(['Suspend! [Result]', 'Loading...']);
464
expect(ReactNoop).toMatchRenderedOutput(null);
465
@@ -581,18 +604,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
604
await waitForAll([]);
605
expect(ReactNoop).toMatchRenderedOutput(null);
606
584
- React.startTransition(() => {
607
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
608
+ React.startTransition(() => {
609
+ ReactNoop.render(<App showA={true} showB={false} />);
610
+ });
611
+ } else {
612
ReactNoop.render(<App showA={true} showB={false} />);
586
- });
613
+ }
614
await waitForAll(['Suspend! [A]', 'Loading...']);
615
expect(ReactNoop).toMatchRenderedOutput(null);
616
617
// Advance React's virtual time by enough to fall into a new async bucket,
618
// but not enough to expire the suspense timeout.
619
ReactNoop.expire(120);
593
- React.startTransition(() => {
620
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
621
+ React.startTransition(() => {
622
+ ReactNoop.render(<App showA={true} showB={true} />);
623
+ });
624
+ } else {
625
ReactNoop.render(<App showA={true} showB={true} />);
595
- });
626
+ }
627
await waitForAll(['Suspend! [A]', 'Loading...']);
628
expect(ReactNoop).toMatchRenderedOutput(null);
629
@@ -674,23 +705,35 @@ describe('ReactSuspenseWithNoopRenderer', () => {
705
706
// Schedule an update at several distinct expiration times
707
await act(async () => {
677
- React.startTransition(() => {
708
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
709
+ React.startTransition(() => {
710
+ root.render(<App step={1} shouldSuspend={true} />);
711
+ });
712
+ } else {
713
root.render(<App step={1} shouldSuspend={true} />);
679
- });
714
+ }
715
Scheduler.unstable_advanceTime(1000);
716
await waitFor(['Sibling']);
717
interrupt();
718
684
- React.startTransition(() => {
719
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
720
+ React.startTransition(() => {
721
+ root.render(<App step={2} shouldSuspend={true} />);
722
+ });
723
+ } else {
724
root.render(<App step={2} shouldSuspend={true} />);
686
- });
725
+ }
726
Scheduler.unstable_advanceTime(1000);
727
await waitFor(['Sibling']);
728
interrupt();
729
691
- React.startTransition(() => {
730
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
731
+ React.startTransition(() => {
732
+ root.render(<App step={3} shouldSuspend={true} />);
733
+ });
734
+ } else {
735
root.render(<App step={3} shouldSuspend={true} />);
693
- });
736
+ }
737
Scheduler.unstable_advanceTime(1000);
738
await waitFor(['Sibling']);
739
interrupt();
@@ -1004,7 +1047,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1047
);
1048
await waitForAll([]);
1049
expect(root).toMatchRenderedOutput(null);
1007
- React.startTransition(() => {
1050
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1051
+ React.startTransition(() => {
1052
+ root.render(
1053
+ <>
1054
+ <Suspense fallback={<Text text="Loading..." />}>
1055
+ <AsyncText text="Async" />
1056
+ <Text text="Sibling" />
1057
+ </Suspense>
1058
+ </>,
1059
+ );
1060
+ });
1061
+ } else {
1062
root.render(
1063
<>
1064
<Suspense fallback={<Text text="Loading..." />}>
@@ -1013,7 +1067,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1067
</Suspense>
1068
</>,
1069
);
1016
- });
1070
+ }
1071
await waitFor(['Suspend! [Async]']);
1072
1073
await resolveText('Async');
@@ -1075,13 +1129,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1129
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
1130
await waitForAll([]);
1131
1078
- React.startTransition(() => {
1132
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1133
+ React.startTransition(() => {
1134
+ ReactNoop.render(
1135
+ <Suspense fallback={<Text text="Loading..." />}>
1136
+ <AsyncText text="Async" />
1137
+ </Suspense>,
1138
+ );
1139
+ });
1140
+ } else {
1141
ReactNoop.render(
1142
<Suspense fallback={<Text text="Loading..." />}>
1143
<AsyncText text="Async" />
1144
</Suspense>,
1145
);
1084
- });
1146
+ }
1147
await waitForAll(['Suspend! [Async]', 'Loading...']);
1148
expect(ReactNoop).toMatchRenderedOutput(null);
1149
@@ -1862,9 +1924,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1924
ReactNoop.render(<Foo />);
1925
await waitForAll(['Foo']);
1926
1865
- React.startTransition(() => {
1927
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1928
+ React.startTransition(() => {
1929
+ ReactNoop.render(<Foo renderContent={true} />);
1930
+ });
1931
+ } else {
1932
ReactNoop.render(<Foo renderContent={true} />);
1867
- });
1933
+ }
1934
Scheduler.unstable_advanceTime(100);
1935
await advanceTimers(100);
1936
// Start rendering
@@ -1893,14 +1959,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1959
await advanceTimers(500);
1960
// No need to rerender.
1961
await waitForAll([]);
1896
- // Since this is a transition, we never fallback.
1897
- expect(ReactNoop).toMatchRenderedOutput(null);
1962
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1963
+ // Since this is a transition, we never fallback.
1964
+ expect(ReactNoop).toMatchRenderedOutput(null);
1965
+ } else {
1966
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
1967
+ }
1968
1969
// Flush the promise completely
1970
await resolveText('A');
1901
- await waitForAll(['Foo', 'A']);
1971
// Renders successfully
1903
- // TODO: Why does this render Foo
1972
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1973
+ // TODO: Why does this render Foo
1974
+ await waitForAll(['Foo', 'A']);
1975
+ } else {
1976
+ await waitForAll(['A']);
1977
+ }
1978
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
1979
});
1980
@@ -2028,9 +2102,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2102
ReactNoop.render(<Foo />);
2103
await waitForAll(['Foo']);
2104
2031
- React.startTransition(() => {
2105
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2106
+ React.startTransition(() => {
2107
+ ReactNoop.render(<Foo renderContent={true} />);
2108
+ });
2109
+ } else {
2110
ReactNoop.render(<Foo renderContent={true} />);
2033
- });
2111
+ }
2112
await waitFor(['Foo']);
2113
2114
// Advance some time.
@@ -2055,8 +2133,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2133
// updates as way earlier in the past. This test ensures that we don't
2134
// use this assumption to add a very long JND.
2135
await waitForAll([]);
2058
- // Transitions never fallback.
2059
- expect(ReactNoop).toMatchRenderedOutput(null);
2136
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2137
+ // Transitions never fallback.
2138
+ expect(ReactNoop).toMatchRenderedOutput(null);
2139
+ } else {
2140
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
2141
+ }
2142
});
2143
2144
// TODO: flip to "warns" when this is implemented again.
@@ -2408,9 +2490,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2490
await waitForAll(['Foo', 'A']);
2491
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2492
2411
- React.startTransition(() => {
2493
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2494
+ React.startTransition(() => {
2495
+ ReactNoop.render(<Foo showB={true} />);
2496
+ });
2497
+ } else {
2498
ReactNoop.render(<Foo showB={true} />);
2413
- });
2499
+ }
2500
2501
await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
2502
// Still suspended.
@@ -2420,8 +2506,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2506
Scheduler.unstable_advanceTime(600);
2507
await advanceTimers(600);
2508
2423
- // Transitions never fall back.
2424
- expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2509
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2510
+ // Transitions never fall back.
2511
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2512
+ } else {
2513
+ expect(ReactNoop).toMatchRenderedOutput(
2514
+ <>
2515
+ <span prop="A" />
2516
+ <span prop="Loading B..." />
2517
+ </>,
2518
+ );
2519
+ }
2520
});
2521
2522
// @gate enableLegacyCache
@@ -2446,9 +2541,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2541
await waitForAll(['Foo', 'A']);
2542
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2543
2449
- React.startTransition(() => {
2544
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2545
+ React.startTransition(() => {
2546
+ ReactNoop.render(<Foo showB={true} />);
2547
+ });
2548
+ } else {
2549
ReactNoop.render(<Foo showB={true} />);
2451
- });
2550
+ }
2551
2552
await waitForAll([
2553
'Foo',
@@ -2463,8 +2562,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2562
Scheduler.unstable_advanceTime(600);
2563
await advanceTimers(600);
2564
2466
- // Transitions never fall back.
2467
- expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2565
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2566
+ // Transitions never fall back.
2567
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2568
+ } else {
2569
+ expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2570
+ }
2571
});
2572
2573
// @gate enableLegacyCache
@@ -3000,18 +3103,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3103
await act(async () => {
3104
// Update. Since showing a fallback would hide content that's already
3105
// visible, it should suspend for a JND without committing.
3003
- React.startTransition(() => {
3106
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
3107
+ React.startTransition(() => {
3108
+ root.render(<App text="First update" />);
3109
+ });
3110
+ } else {
3111
root.render(<App text="First update" />);
3005
- });
3112
+ }
3113
await waitForAll(['Suspend! [First update]']);
3114
3115
// Should not display a fallback
3116
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
3117
3118
// Update again. This should also suspend for a JND.
3012
- React.startTransition(() => {
3119
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
3120
+ React.startTransition(() => {
3121
+ root.render(<App text="Second update" />);
3122
+ });
3123
+ } else {
3124
root.render(<App text="Second update" />);
3014
- });
3125
+ }
3126
await waitForAll(['Suspend! [Second update]']);
3127
3128
// Should not display a fallback
@@ -3775,6 +3886,117 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3886
);
3887
});
3888
3889
+ // @gate enableLegacyCache
3890
+ // @gate !enableSyncDefaultUpdates
3891
+ it('regression: ping at high priority causes update to be dropped', async () => {
3892
+ const {useState, useTransition} = React;
3893
+
3894
+ let setTextA;
3895
+ function A() {
3896
+ const [textA, _setTextA] = useState('A');
3897
+ setTextA = _setTextA;
3898
+ return (
3899
+ <Suspense fallback={<Text text="Loading..." />}>
3900
+ <AsyncText text={textA} />
3901
+ </Suspense>
3902
+ );
3903
+ }
3904
+
3905
+ let setTextB;
3906
+ let startTransitionFromB;
3907
+ function B() {
3908
+ const [textB, _setTextB] = useState('B');
3909
+ // eslint-disable-next-line no-unused-vars
3910
+ const [_, _startTransition] = useTransition();
3911
+ startTransitionFromB = _startTransition;
3912
+ setTextB = _setTextB;
3913
+ return (
3914
+ <Suspense fallback={<Text text="Loading..." />}>
3915
+ <AsyncText text={textB} />
3916
+ </Suspense>
3917
+ );
3918
+ }
3919
+
3920
+ function App() {
3921
+ return (
3922
+ <>
3923
+ <A />
3924
+ <B />
3925
+ </>
3926
+ );
3927
+ }
3928
+
3929
+ const root = ReactNoop.createRoot();
3930
+ await act(async () => {
3931
+ await seedNextTextCache('A');
3932
+ await seedNextTextCache('B');
3933
+ root.render(<App />);
3934
+ });
3935
+ assertLog(['A', 'B']);
3936
+ expect(root).toMatchRenderedOutput(
3937
+ <>
3938
+ <span prop="A" />
3939
+ <span prop="B" />
3940
+ </>,
3941
+ );
3942
+
3943
+ await act(async () => {
3944
+ // Triggers suspense at normal pri
3945
+ setTextA('A1');
3946
+ // Triggers in an unrelated tree at a different pri
3947
+ startTransitionFromB(() => {
3948
+ // Update A again so that it doesn't suspend on A1. That way we can ping
3949
+ // the A1 update without also pinging this one. This is a workaround
3950
+ // because there's currently no way to render at a lower priority (B2)
3951
+ // without including all updates at higher priority (A1).
3952
+ setTextA('A2');
3953
+ setTextB('B2');
3954
+ });
3955
+
3956
+ await waitFor([
3957
+ 'B',
3958
+ 'Suspend! [A1]',
3959
+ 'Loading...',
3960
+
3961
+ 'Suspend! [A2]',
3962
+ 'Loading...',
3963
+ 'Suspend! [B2]',
3964
+ 'Loading...',
3965
+ ]);
3966
+ expect(root).toMatchRenderedOutput(
3967
+ <>
3968
+ <span prop="A" />
3969
+ <span prop="B" />
3970
+ </>,
3971
+ );
3972
+
3973
+ await resolveText('A1');
3974
+ await waitFor([
3975
+ 'A1',
3976
+ 'Suspend! [A2]',
3977
+ 'Loading...',
3978
+ 'Suspend! [B2]',
3979
+ 'Loading...',
3980
+ ]);
3981
+ expect(root).toMatchRenderedOutput(
3982
+ <>
3983
+ <span prop="A1" />
3984
+ <span prop="B" />
3985
+ </>,
3986
+ );
3987
+
3988
+ await resolveText('A2');
3989
+ await resolveText('B2');
3990
+ });
3991
+ assertLog(['A2', 'B2']);
3992
+ expect(root).toMatchRenderedOutput(
3993
+ <>
3994
+ <span prop="A2" />
3995
+ <span prop="B2" />
3996
+ </>,
3997
+ );
3998
+ });
3999
+
4000
// Regression: https://github.com/facebook/react/issues/18486
4001
// @gate enableLegacyCache
4002
it('does not get stuck in pending state with render phase updates', async () => {
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+172
-29
@@ -221,7 +221,27 @@ describe('useMutableSource', () => {
221
const mutableSource = createMutableSource(source, param => param.version);
222
223
await act(async () => {
224
- React.startTransition(() => {
224
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
225
+ React.startTransition(() => {
226
+ ReactNoop.render(
227
+ <>
228
+ <Component
229
+ label="a"
230
+ getSnapshot={defaultGetSnapshot}
231
+ mutableSource={mutableSource}
232
+ subscribe={defaultSubscribe}
233
+ />
234
+ <Component
235
+ label="b"
236
+ getSnapshot={defaultGetSnapshot}
237
+ mutableSource={mutableSource}
238
+ subscribe={defaultSubscribe}
239
+ />
240
+ </>,
241
+ () => Scheduler.log('Sync effect'),
242
+ );
243
+ });
244
+ } else {
245
ReactNoop.render(
246
<>
247
<Component
@@ -239,7 +259,7 @@ describe('useMutableSource', () => {
259
</>,
260
() => Scheduler.log('Sync effect'),
261
);
242
- });
262
+ }
263
// Do enough work to read from one component
264
await waitFor(['a:one']);
265
@@ -436,9 +456,13 @@ describe('useMutableSource', () => {
456
457
// Changing values should schedule an update with React.
458
// Start working on this update but don't finish it.
439
- React.startTransition(() => {
459
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
460
+ React.startTransition(() => {
461
+ source.value = 'two';
462
+ });
463
+ } else {
464
source.value = 'two';
441
- });
465
+ }
466
await waitFor(['a:two']);
467
468
// Re-renders that occur before the update is processed
@@ -696,7 +720,33 @@ describe('useMutableSource', () => {
720
721
// Because the store has not changed yet, there are no pending updates,
722
// so it is considered safe to read from when we start this render.
699
- React.startTransition(() => {
723
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
724
+ React.startTransition(() => {
725
+ ReactNoop.render(
726
+ <>
727
+ <Component
728
+ label="a"
729
+ getSnapshot={getSnapshotA}
730
+ mutableSource={mutableSource}
731
+ subscribe={subscribeA}
732
+ />
733
+ <Component
734
+ label="b"
735
+ getSnapshot={getSnapshotB}
736
+ mutableSource={mutableSource}
737
+ subscribe={subscribeB}
738
+ />
739
+ <Component
740
+ label="c"
741
+ getSnapshot={getSnapshotB}
742
+ mutableSource={mutableSource}
743
+ subscribe={subscribeB}
744
+ />
745
+ </>,
746
+ () => Scheduler.log('Sync effect'),
747
+ );
748
+ });
749
+ } else {
750
ReactNoop.render(
751
<>
752
<Component
@@ -720,7 +770,7 @@ describe('useMutableSource', () => {
770
</>,
771
() => Scheduler.log('Sync effect'),
772
);
723
- });
773
+ }
774
await waitFor(['a:a:one', 'b:b:one']);
775
776
// Mutating the source should trigger a tear detection on the next read,
@@ -806,7 +856,26 @@ describe('useMutableSource', () => {
856
857
await act(async () => {
858
// Start a render that uses the mutable source.
809
- React.startTransition(() => {
859
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
860
+ React.startTransition(() => {
861
+ ReactNoop.render(
862
+ <>
863
+ <Component
864
+ label="a"
865
+ getSnapshot={defaultGetSnapshot}
866
+ mutableSource={mutableSource}
867
+ subscribe={defaultSubscribe}
868
+ />
869
+ <Component
870
+ label="b"
871
+ getSnapshot={defaultGetSnapshot}
872
+ mutableSource={mutableSource}
873
+ subscribe={defaultSubscribe}
874
+ />
875
+ </>,
876
+ );
877
+ });
878
+ } else {
879
ReactNoop.render(
880
<>
881
<Component
@@ -823,7 +892,7 @@ describe('useMutableSource', () => {
892
/>
893
</>,
894
);
826
- });
895
+ }
896
await waitFor(['a:one']);
897
898
// Mutate source
@@ -1455,7 +1524,17 @@ describe('useMutableSource', () => {
1524
expect(root).toMatchRenderedOutput('a0');
1525
1526
await act(async () => {
1458
- React.startTransition(() => {
1527
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1528
+ React.startTransition(() => {
1529
+ root.render(
1530
+ <>
1531
+ <Read getSnapshot={getSnapshotA} />
1532
+ <Read getSnapshot={getSnapshotB} />
1533
+ <Text text="c" />
1534
+ </>,
1535
+ );
1536
+ });
1537
+ } else {
1538
root.render(
1539
<>
1540
<Read getSnapshot={getSnapshotA} />
@@ -1463,7 +1542,7 @@ describe('useMutableSource', () => {
1542
<Text text="c" />
1543
</>,
1544
);
1466
- });
1545
+ }
1546
1547
await waitFor(['a0', 'b0']);
1548
// Mutate in an event. This schedules a subscription update on a, which
@@ -1597,9 +1676,13 @@ describe('useMutableSource', () => {
1676
1677
await act(async () => {
1678
// Switch the parent and the child to read using the same config
1600
- React.startTransition(() => {
1679
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1680
+ React.startTransition(() => {
1681
+ root.render(<App parentConfig={configB} childConfig={configB} />);
1682
+ });
1683
+ } else {
1684
root.render(<App parentConfig={configB} childConfig={configB} />);
1602
- });
1685
+ }
1686
// Start rendering the parent, but yield before rendering the child
1687
await waitFor(['Parent: 2']);
1688
@@ -1610,19 +1693,41 @@ describe('useMutableSource', () => {
1693
source.valueB = '3';
1694
});
1695
1613
- // In default sync mode, all of the updates flush sync.
1614
- await waitFor([
1615
- // The partial render completes
1616
- 'Child: 2',
1617
- 'Commit: 2, 2',
1618
- 'Parent: 3',
1619
- 'Child: 3',
1620
- ]);
1621
-
1622
- await waitForAll([
1623
- // Now finish the rest of the update
1624
- 'Commit: 3, 3',
1625
- ]);
1696
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1697
+ // In default sync mode, all of the updates flush sync.
1698
+ await waitFor([
1699
+ // The partial render completes
1700
+ 'Child: 2',
1701
+ 'Commit: 2, 2',
1702
+ 'Parent: 3',
1703
+ 'Child: 3',
1704
+ ]);
1705
+
1706
+ await waitForAll([
1707
+ // Now finish the rest of the update
1708
+ 'Commit: 3, 3',
1709
+ ]);
1710
+ } else {
1711
+ await waitFor([
1712
+ // The partial render completes
1713
+ 'Child: 2',
1714
+ 'Commit: 2, 2',
1715
+ ]);
1716
+
1717
+ // Now there are two pending mutations at different priorities. But they
1718
+ // both read the same version of the mutable source, so we must render
1719
+ // them simultaneously.
1720
+ //
1721
+ await waitFor([
1722
+ 'Parent: 3',
1723
+ // Demonstrates that we can yield here
1724
+ ]);
1725
+ await waitFor([
1726
+ // Now finish the rest of the update
1727
+ 'Child: 3',
1728
+ 'Commit: 3, 3',
1729
+ ]);
1730
+ }
1731
});
1732
});
1733
@@ -1738,7 +1843,26 @@ describe('useMutableSource', () => {
1843
1844
await act(async () => {
1845
// Start a render that uses the mutable source.
1741
- React.startTransition(() => {
1846
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1847
+ React.startTransition(() => {
1848
+ ReactNoop.render(
1849
+ <>
1850
+ <Component
1851
+ label="a"
1852
+ getSnapshot={defaultGetSnapshot}
1853
+ mutableSource={mutableSource}
1854
+ subscribe={defaultSubscribe}
1855
+ />
1856
+ <Component
1857
+ label="b"
1858
+ getSnapshot={defaultGetSnapshot}
1859
+ mutableSource={mutableSource}
1860
+ subscribe={defaultSubscribe}
1861
+ />
1862
+ </>,
1863
+ );
1864
+ });
1865
+ } else {
1866
ReactNoop.render(
1867
<>
1868
<Component
@@ -1755,7 +1879,7 @@ describe('useMutableSource', () => {
1879
/>
1880
</>,
1881
);
1758
- });
1882
+ }
1883
await waitFor(['a:one']);
1884
1885
const PrevScheduler = Scheduler;
@@ -1800,7 +1924,26 @@ describe('useMutableSource', () => {
1924
1925
await act(async () => {
1926
// Start a render that uses the mutable source.
1803
- React.startTransition(() => {
1927
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1928
+ React.startTransition(() => {
1929
+ ReactNoop.render(
1930
+ <>
1931
+ <Component
1932
+ label="a"
1933
+ getSnapshot={defaultGetSnapshot}
1934
+ mutableSource={mutableSource}
1935
+ subscribe={defaultSubscribe}
1936
+ />
1937
+ <Component
1938
+ label="b"
1939
+ getSnapshot={defaultGetSnapshot}
1940
+ mutableSource={mutableSource}
1941
+ subscribe={defaultSubscribe}
1942
+ />
1943
+ </>,
1944
+ );
1945
+ });
1946
+ } else {
1947
ReactNoop.render(
1948
<>
1949
<Component
@@ -1817,7 +1960,7 @@ describe('useMutableSource', () => {
1960
/>
1961
</>,
1962
);
1820
- });
1963
+ }
1964
await waitFor(['a:one']);
1965
1966
const PrevScheduler = Scheduler;
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+11
-2
@@ -260,14 +260,23 @@ describe('useMutableSourceHydration', () => {
260
261
await expect(async () => {
262
await act(async () => {
263
- React.startTransition(() => {
263
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
264
+ React.startTransition(() => {
265
+ ReactDOMClient.hydrateRoot(container, <TestComponent />, {
266
+ mutableSources: [mutableSource],
267
+ onRecoverableError(error) {
268
+ Scheduler.log('Log error: ' + error.message);
269
+ },
270
+ });
271
+ });
272
+ } else {
273
ReactDOMClient.hydrateRoot(container, <TestComponent />, {
274
mutableSources: [mutableSource],
275
onRecoverableError(error) {
276
Scheduler.log('Log error: ' + error.message);
277
},
278
});
270
- });
279
+ }
280
await waitFor(['a:one']);
281
source.value = 'two';
282
});
packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+16
-4
@@ -95,11 +95,17 @@ describe('ReactTestRendererAsync', () => {
95
}
96
97
let renderer;
98
- React.startTransition(() => {
98
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
99
+ React.startTransition(() => {
100
+ renderer = ReactTestRenderer.create(<Parent step={1} />, {
101
+ unstable_isConcurrent: true,
102
+ });
103
+ });
104
+ } else {
105
renderer = ReactTestRenderer.create(<Parent step={1} />, {
106
unstable_isConcurrent: true,
107
});
102
- });
108
+ }
109
110
// Flush the first two siblings
111
await waitFor(['A:1', 'B:1']);
@@ -135,11 +141,17 @@ describe('ReactTestRendererAsync', () => {
141
}
142
143
let renderer;
138
- React.startTransition(() => {
144
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
145
+ React.startTransition(() => {
146
+ renderer = ReactTestRenderer.create(<Example step={1} />, {
147
+ unstable_isConcurrent: true,
148
+ });
149
+ });
150
+ } else {
151
renderer = ReactTestRenderer.create(<Example step={1} />, {
152
unstable_isConcurrent: true,
153
});
142
- });
154
+ }
155
156
// Flush the some of the changes, but don't commit
157
await waitFor(['A:1']);
packages/react/src/__tests__/ReactProfiler-test.internal.js
+71
-12
@@ -206,7 +206,19 @@ describe(`onRender`, () => {
206
return null;
207
};
208
209
- React.startTransition(() => {
209
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
210
+ React.startTransition(() => {
211
+ ReactTestRenderer.create(
212
+ <React.Profiler id="test" onRender={callback}>
213
+ <Yield value="first" />
214
+ <Yield value="last" />
215
+ </React.Profiler>,
216
+ {
217
+ unstable_isConcurrent: true,
218
+ },
219
+ );
220
+ });
221
+ } else {
222
ReactTestRenderer.create(
223
<React.Profiler id="test" onRender={callback}>
224
<Yield value="first" />
@@ -216,7 +228,7 @@ describe(`onRender`, () => {
228
unstable_isConcurrent: true,
229
},
230
);
219
- });
231
+ }
232
233
// Times are logged until a render is committed.
234
await waitFor(['first']);
@@ -751,7 +763,17 @@ describe(`onRender`, () => {
763
Scheduler.unstable_advanceTime(5); // 0 -> 5
764
765
// Render partially, but run out of time before completing.
754
- React.startTransition(() => {
766
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
767
+ React.startTransition(() => {
768
+ ReactTestRenderer.create(
769
+ <React.Profiler id="test" onRender={callback}>
770
+ <Yield renderTime={2} />
771
+ <Yield renderTime={3} />
772
+ </React.Profiler>,
773
+ {unstable_isConcurrent: true},
774
+ );
775
+ });
776
+ } else {
777
ReactTestRenderer.create(
778
<React.Profiler id="test" onRender={callback}>
779
<Yield renderTime={2} />
@@ -759,7 +781,7 @@ describe(`onRender`, () => {
781
</React.Profiler>,
782
{unstable_isConcurrent: true},
783
);
762
- });
784
+ }
785
await waitFor(['Yield:2']);
786
expect(callback).toHaveBeenCalledTimes(0);
787
@@ -788,7 +810,20 @@ describe(`onRender`, () => {
810
811
// Render partially, but don't finish.
812
// This partial render should take 5ms of simulated time.
791
- React.startTransition(() => {
813
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
814
+ React.startTransition(() => {
815
+ ReactTestRenderer.create(
816
+ <React.Profiler id="outer" onRender={callback}>
817
+ <Yield renderTime={5} />
818
+ <Yield renderTime={10} />
819
+ <React.Profiler id="inner" onRender={callback}>
820
+ <Yield renderTime={17} />
821
+ </React.Profiler>
822
+ </React.Profiler>,
823
+ {unstable_isConcurrent: true},
824
+ );
825
+ });
826
+ } else {
827
ReactTestRenderer.create(
828
<React.Profiler id="outer" onRender={callback}>
829
<Yield renderTime={5} />
@@ -799,7 +834,7 @@ describe(`onRender`, () => {
834
</React.Profiler>,
835
{unstable_isConcurrent: true},
836
);
802
- });
837
+ }
838
await waitFor(['Yield:5']);
839
expect(callback).toHaveBeenCalledTimes(0);
840
@@ -841,7 +876,17 @@ describe(`onRender`, () => {
876
// Render a partially update, but don't finish.
877
// This partial render should take 10ms of simulated time.
878
let renderer;
844
- React.startTransition(() => {
879
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
880
+ React.startTransition(() => {
881
+ renderer = ReactTestRenderer.create(
882
+ <React.Profiler id="test" onRender={callback}>
883
+ <Yield renderTime={10} />
884
+ <Yield renderTime={20} />
885
+ </React.Profiler>,
886
+ {unstable_isConcurrent: true},
887
+ );
888
+ });
889
+ } else {
890
renderer = ReactTestRenderer.create(
891
<React.Profiler id="test" onRender={callback}>
892
<Yield renderTime={10} />
@@ -849,7 +894,7 @@ describe(`onRender`, () => {
894
</React.Profiler>,
895
{unstable_isConcurrent: true},
896
);
852
- });
897
+ }
898
await waitFor(['Yield:10']);
899
expect(callback).toHaveBeenCalledTimes(0);
900
@@ -918,7 +963,17 @@ describe(`onRender`, () => {
963
964
// Render a partially update, but don't finish.
965
// This partial render should take 3ms of simulated time.
921
- React.startTransition(() => {
966
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
967
+ React.startTransition(() => {
968
+ renderer.update(
969
+ <React.Profiler id="test" onRender={callback}>
970
+ <Yield renderTime={3} />
971
+ <Yield renderTime={5} />
972
+ <Yield renderTime={9} />
973
+ </React.Profiler>,
974
+ );
975
+ });
976
+ } else {
977
renderer.update(
978
<React.Profiler id="test" onRender={callback}>
979
<Yield renderTime={3} />
@@ -926,7 +981,7 @@ describe(`onRender`, () => {
981
<Yield renderTime={9} />
982
</React.Profiler>,
983
);
929
- });
984
+ }
985
await waitFor(['Yield:3']);
986
expect(callback).toHaveBeenCalledTimes(0);
987
@@ -1028,9 +1083,13 @@ describe(`onRender`, () => {
1083
1084
// Render a partially update, but don't finish.
1085
// This partial render will take 10ms of actual render time.
1031
- React.startTransition(() => {
1086
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1087
+ React.startTransition(() => {
1088
+ first.setState({renderTime: 10});
1089
+ });
1090
+ } else {
1091
first.setState({renderTime: 10});
1033
- });
1092
+ }
1093
await waitFor(['FirstComponent:10']);
1094
expect(callback).toHaveBeenCalledTimes(0);
1095
packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
+6
-2
@@ -161,9 +161,13 @@ describe('ReactProfiler DevTools integration', () => {
161
// for updates.
162
Scheduler.unstable_advanceTime(10000);
163
// Schedule an update.
164
- React.startTransition(() => {
164
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
165
+ React.startTransition(() => {
166
+ root.update(<Text text="B" />);
167
+ });
168
+ } else {
169
root.update(<Text text="B" />);
166
- });
170
+ }
171
172
// Update B should not instantly expire.
173
await waitFor([]);
packages/shared/ReactFeatureFlags.js
+3
@@ -142,6 +142,9 @@ export const disableLegacyContext = false;
142
143
export const enableUseRefAccessWarning = false;
144
145
+// Enables time slicing for updates that aren't wrapped in startTransition.
146
+export const enableSyncDefaultUpdates = true;
147
+
148
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
149
150
// Adds an opt-in to time slicing for updates that aren't wrapped in
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -64,6 +64,7 @@ export const createRootStrictEffectsByDefault = false;
64
export const disableSchedulerTimeoutInWorkLoop = false;
65
export const enableLazyContextPropagation = false;
66
export const enableLegacyHidden = true;
67
+export const enableSyncDefaultUpdates = true;
68
export const enableUnifiedSyncLane = false;
69
export const allowConcurrentByDefault = true;
70
export const enableCustomElementPropertySupport = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
54
export const disableSchedulerTimeoutInWorkLoop = false;
55
export const enableLazyContextPropagation = false;
56
export const enableLegacyHidden = false;
57
+export const enableSyncDefaultUpdates = true;
58
export const enableUnifiedSyncLane = false;
59
export const allowConcurrentByDefault = false;
60
export const enableCustomElementPropertySupport = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
54
export const disableSchedulerTimeoutInWorkLoop = false;
55
export const enableLazyContextPropagation = false;
56
export const enableLegacyHidden = false;
57
+export const enableSyncDefaultUpdates = true;
58
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
59
export const allowConcurrentByDefault = false;
60
export const enableCustomElementPropertySupport = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -53,6 +53,7 @@ export const enableUseRefAccessWarning = false;
53
export const disableSchedulerTimeoutInWorkLoop = false;
54
export const enableLazyContextPropagation = false;
55
export const enableLegacyHidden = false;
56
+export const enableSyncDefaultUpdates = true;
57
export const enableUnifiedSyncLane = false;
58
export const allowConcurrentByDefault = true;
59
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
54
export const disableSchedulerTimeoutInWorkLoop = false;
55
export const enableLazyContextPropagation = false;
56
export const enableLegacyHidden = false;
57
+export const enableSyncDefaultUpdates = true;
58
export const enableUnifiedSyncLane = false;
59
export const allowConcurrentByDefault = true;
60
export const enableCustomElementPropertySupport = false;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -20,6 +20,7 @@ export const enableUseRefAccessWarning = __VARIANT__;
20
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
21
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
22
export const enableLazyContextPropagation = __VARIANT__;
23
+export const enableSyncDefaultUpdates = __VARIANT__;
24
export const enableUnifiedSyncLane = __VARIANT__;
25
export const enableTransitionTracing = __VARIANT__;
26
export const enableCustomElementPropertySupport = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -24,6 +24,7 @@ export const {
24
enableDebugTracing,
25
enableUseRefAccessWarning,
26
enableLazyContextPropagation,
27
+ enableSyncDefaultUpdates,
28
enableUnifiedSyncLane,
29
enableTransitionTracing,
30
enableCustomElementPropertySupport,
packages/use-subscription/src/__tests__/useSubscription-test.js
+24
-8
@@ -339,9 +339,13 @@ describe('useSubscription', () => {
339
340
// Start React update, but don't finish
341
await act(async () => {
342
- React.startTransition(() => {
342
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
343
+ React.startTransition(() => {
344
+ renderer.update(<Parent observed={observableB} />);
345
+ });
346
+ } else {
347
renderer.update(<Parent observed={observableB} />);
344
- });
348
+ }
349
350
await waitFor(['Child: b-0']);
351
expect(log).toEqual(['Parent.componentDidMount']);
@@ -443,9 +447,13 @@ describe('useSubscription', () => {
447
448
// Start React update, but don't finish
449
await act(async () => {
446
- React.startTransition(() => {
450
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
451
+ React.startTransition(() => {
452
+ renderer.update(<Parent observed={observableB} />);
453
+ });
454
+ } else {
455
renderer.update(<Parent observed={observableB} />);
448
- });
456
+ }
457
await waitFor(['Child: b-0']);
458
expect(log).toEqual([]);
459
@@ -624,13 +632,21 @@ describe('useSubscription', () => {
632
// Interrupt with a second mutation "C" -> "D".
633
// This update will not be eagerly evaluated,
634
// but useSubscription() should eagerly close over the updated value to avoid tearing.
627
- React.startTransition(() => {
635
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
636
+ React.startTransition(() => {
637
+ mutate('C');
638
+ });
639
+ } else {
640
mutate('C');
629
- });
641
+ }
642
await waitFor(['render:first:C', 'render:second:C']);
631
- React.startTransition(() => {
643
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
644
+ React.startTransition(() => {
645
+ mutate('D');
646
+ });
647
+ } else {
648
mutate('D');
633
- });
649
+ }
650
await waitForAll(['render:first:D', 'render:second:D']);
651
652
// No more pending updates