Make time-slicing opt-in (#21072)
* Add enableSyncDefaultUpdates feature flag * Add enableSyncDefaultUpdates implementation * Fix tests * Switch feature flag to true by default * Finish concurrent render whenever for non-sync lanes * Also return DefaultLane with eventLane * Gate interruption test * Add continuout native event test * Fix tests from rebasing main * Hardcode lanes, remove added export * Sync forks
Ricky committed
Apr 9, 2021 at 19:50 UTC
933880b4544a83ce54c8a47f348effe725a58843
46 files changed
+1996
-538
packages/create-subscription/src/__tests__/createSubscription-test.js
+16
-2
@@ -268,6 +268,7 @@ describe('createSubscription', () => {
268
expect(Scheduler).toFlushAndYield(['b-1']);
269
});
270
271
+ // @gate experimental || !enableSyncDefaultUpdates
272
it('should ignore values emitted by a new subscribable until the commit phase', () => {
273
const log = [];
274
@@ -325,7 +326,13 @@ describe('createSubscription', () => {
326
expect(log).toEqual(['Parent.componentDidMount']);
327
328
// Start React update, but don't finish
328
- ReactNoop.render(<Parent observed={observableB} />);
329
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
330
+ React.unstable_startTransition(() => {
331
+ ReactNoop.render(<Parent observed={observableB} />);
332
+ });
333
+ } else {
334
+ ReactNoop.render(<Parent observed={observableB} />);
335
+ }
336
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
337
expect(log).toEqual(['Parent.componentDidMount']);
338
@@ -355,6 +362,7 @@ describe('createSubscription', () => {
362
]);
363
});
364
365
+ // @gate experimental || !enableSyncDefaultUpdates
366
it('should not drop values emitted between updates', () => {
367
const log = [];
368
@@ -412,7 +420,13 @@ describe('createSubscription', () => {
420
expect(log).toEqual(['Parent.componentDidMount']);
421
422
// Start React update, but don't finish
415
- ReactNoop.render(<Parent observed={observableB} />);
423
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
424
+ React.unstable_startTransition(() => {
425
+ ReactNoop.render(<Parent observed={observableB} />);
426
+ });
427
+ } else {
428
+ ReactNoop.render(<Parent observed={observableB} />);
429
+ }
430
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
431
expect(log).toEqual(['Parent.componentDidMount']);
432
packages/react-art/src/__tests__/ReactART-test.js
+1
@@ -360,6 +360,7 @@ describe('ReactART', () => {
360
expect(onClick2).toBeCalled();
361
});
362
363
+ // @gate !enableSyncDefaultUpdates
364
it('can concurrently render with a "primary" renderer while sharing context', () => {
365
const CurrentRendererContext = React.createContext(null);
366
packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
+40
@@ -284,6 +284,46 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
284
expect(container.textContent).toEqual('hovered');
285
});
286
287
+ // @gate experimental
288
+ it('continuous native events flush as expected', async () => {
289
+ const root = ReactDOM.unstable_createRoot(container);
290
+
291
+ const target = React.createRef(null);
292
+ function Foo({hovered}) {
293
+ const hoverString = hovered ? 'hovered' : 'not hovered';
294
+ Scheduler.unstable_yieldValue(hoverString);
295
+ return <div ref={target}>{hoverString}</div>;
296
+ }
297
+
298
+ await act(async () => {
299
+ root.render(<Foo hovered={false} />);
300
+ });
301
+ expect(container.textContent).toEqual('not hovered');
302
+
303
+ await act(async () => {
304
+ // Note: React does not use native mouseenter/mouseleave events
305
+ // but we should still correctly determine their priority.
306
+ const mouseEnterEvent = document.createEvent('MouseEvents');
307
+ mouseEnterEvent.initEvent('mouseover', true, true);
308
+ target.current.addEventListener('mouseover', () => {
309
+ root.render(<Foo hovered={true} />);
310
+ });
311
+ dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
312
+
313
+ // Since mouse end is not discrete, should not have updated yet
314
+ expect(Scheduler).toHaveYielded(['not hovered']);
315
+ expect(container.textContent).toEqual('not hovered');
316
+
317
+ expect(Scheduler).toFlushAndYieldThrough(['hovered']);
318
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
319
+ expect(container.textContent).toEqual('hovered');
320
+ } else {
321
+ expect(container.textContent).toEqual('not hovered');
322
+ }
323
+ });
324
+ expect(container.textContent).toEqual('hovered');
325
+ });
326
+
327
// @gate experimental
328
it('should batch inside native events', async () => {
329
const root = ReactDOM.unstable_createRoot(container);
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+15
-5
@@ -1865,11 +1865,21 @@ describe('ReactDOMServerPartialHydration', () => {
1865
suspend = true;
1866
1867
await act(async () => {
1868
- root.render(<App />);
1869
- expect(Scheduler).toFlushAndYieldThrough(['Before']);
1870
- // This took a long time to render.
1871
- Scheduler.unstable_advanceTime(1000);
1872
- expect(Scheduler).toFlushAndYield(['After']);
1868
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1869
+ React.unstable_startTransition(() => {
1870
+ root.render(<App />);
1871
+ });
1872
+
1873
+ expect(Scheduler).toFlushAndYieldThrough(['Before', 'After']);
1874
+ } else {
1875
+ root.render(<App />);
1876
+
1877
+ expect(Scheduler).toFlushAndYieldThrough(['Before']);
1878
+ // This took a long time to render.
1879
+ Scheduler.unstable_advanceTime(1000);
1880
+ expect(Scheduler).toFlushAndYield(['After']);
1881
+ }
1882
+
1883
// This will cause us to skip the second row completely.
1884
});
1885
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+7
-1
@@ -1934,7 +1934,13 @@ describe('DOMPluginEventSystem', () => {
1934
log.length = 0;
1935
1936
// Increase counter
1937
- root.render(<Test counter={1} />);
1937
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1938
+ React.unstable_startTransition(() => {
1939
+ root.render(<Test counter={1} />);
1940
+ });
1941
+ } else {
1942
+ root.render(<Test counter={1} />);
1943
+ }
1944
// Yield before committing
1945
expect(Scheduler).toFlushAndYieldThrough(['Test']);
1946
packages/react-reconciler/src/ReactFiberLane.new.js
+1
-1
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
52
53
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
54
55
-const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
55
+export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
56
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
57
58
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
packages/react-reconciler/src/ReactFiberLane.old.js
+1
-1
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
52
53
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
54
55
-const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
55
+export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
56
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
57
58
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+34
-2
@@ -32,6 +32,7 @@ import {
32
disableSchedulerTimeoutInWorkLoop,
33
enableStrictEffects,
34
skipUnmountedBoundaries,
35
+ enableSyncDefaultUpdates,
36
enableUpdaterTracking,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
139
NoLanes,
140
NoLane,
141
SyncLane,
142
+ DefaultLane,
143
+ DefaultHydrationLane,
144
+ InputContinuousLane,
145
+ InputContinuousHydrationLane,
146
NoTimestamp,
147
claimNextTransitionLane,
148
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
438
// TODO: Move this type conversion to the event priority module.
439
const updateLane: Lane = (getCurrentUpdatePriority(): any);
440
if (updateLane !== NoLane) {
441
+ if (
442
+ enableSyncDefaultUpdates &&
443
+ (updateLane === InputContinuousLane ||
444
+ updateLane === InputContinuousHydrationLane)
445
+ ) {
446
+ return DefaultLane;
447
+ }
448
return updateLane;
449
}
450
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
455
// use that directly.
456
// TODO: Move this type conversion to the event priority module.
457
const eventLane: Lane = (getCurrentEventPriority(): any);
458
+ if (
459
+ enableSyncDefaultUpdates &&
460
+ (eventLane === InputContinuousLane ||
461
+ eventLane === InputContinuousHydrationLane)
462
+ ) {
463
+ return DefaultLane;
464
+ }
465
return eventLane;
466
}
467
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
714
715
// Schedule a new callback.
716
let newCallbackNode;
698
- if (newCallbackPriority === SyncLane) {
717
+ if (
718
+ enableSyncDefaultUpdates &&
719
+ (newCallbackPriority === DefaultLane ||
720
+ newCallbackPriority === DefaultHydrationLane)
721
+ ) {
722
+ newCallbackNode = scheduleCallback(
723
+ ImmediateSchedulerPriority,
724
+ performSyncWorkOnRoot.bind(null, root),
725
+ );
726
+ } else if (newCallbackPriority === SyncLane) {
727
// Special case: Sync React callbacks are scheduled on a special
728
// internal queue
729
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
1058
const finishedWork: Fiber = (root.current.alternate: any);
1059
root.finishedWork = finishedWork;
1060
root.finishedLanes = lanes;
1033
- commitRoot(root);
1061
+ if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
1062
+ finishConcurrentRender(root, exitStatus, lanes);
1063
+ } else {
1064
+ commitRoot(root);
1065
+ }
1066
1067
// Before exiting, make sure there's a callback scheduled for the next
1068
// pending level.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+34
-2
@@ -32,6 +32,7 @@ import {
32
disableSchedulerTimeoutInWorkLoop,
33
enableStrictEffects,
34
skipUnmountedBoundaries,
35
+ enableSyncDefaultUpdates,
36
enableUpdaterTracking,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
139
NoLanes,
140
NoLane,
141
SyncLane,
142
+ DefaultLane,
143
+ DefaultHydrationLane,
144
+ InputContinuousLane,
145
+ InputContinuousHydrationLane,
146
NoTimestamp,
147
claimNextTransitionLane,
148
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
438
// TODO: Move this type conversion to the event priority module.
439
const updateLane: Lane = (getCurrentUpdatePriority(): any);
440
if (updateLane !== NoLane) {
441
+ if (
442
+ enableSyncDefaultUpdates &&
443
+ (updateLane === InputContinuousLane ||
444
+ updateLane === InputContinuousHydrationLane)
445
+ ) {
446
+ return DefaultLane;
447
+ }
448
return updateLane;
449
}
450
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
455
// use that directly.
456
// TODO: Move this type conversion to the event priority module.
457
const eventLane: Lane = (getCurrentEventPriority(): any);
458
+ if (
459
+ enableSyncDefaultUpdates &&
460
+ (eventLane === InputContinuousLane ||
461
+ eventLane === InputContinuousHydrationLane)
462
+ ) {
463
+ return DefaultLane;
464
+ }
465
return eventLane;
466
}
467
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
714
715
// Schedule a new callback.
716
let newCallbackNode;
698
- if (newCallbackPriority === SyncLane) {
717
+ if (
718
+ enableSyncDefaultUpdates &&
719
+ (newCallbackPriority === DefaultLane ||
720
+ newCallbackPriority === DefaultHydrationLane)
721
+ ) {
722
+ newCallbackNode = scheduleCallback(
723
+ ImmediateSchedulerPriority,
724
+ performSyncWorkOnRoot.bind(null, root),
725
+ );
726
+ } else if (newCallbackPriority === SyncLane) {
727
// Special case: Sync React callbacks are scheduled on a special
728
// internal queue
729
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
1058
const finishedWork: Fiber = (root.current.alternate: any);
1059
root.finishedWork = finishedWork;
1060
root.finishedLanes = lanes;
1033
- commitRoot(root);
1061
+ if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
1062
+ finishConcurrentRender(root, exitStatus, lanes);
1063
+ } else {
1064
+ commitRoot(root);
1065
+ }
1066
1067
// Before exiting, make sure there's a callback scheduled for the next
1068
// pending level.
packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
+8
-1
@@ -45,6 +45,7 @@ describe('ReactSuspenseList', () => {
45
return Component;
46
}
47
48
+ // @gate experimental || !enableSyncDefaultUpdates
49
it('appends rendering tasks to the end of the priority queue', async () => {
50
const A = createAsyncText('A');
51
const B = createAsyncText('B');
@@ -63,7 +64,13 @@ describe('ReactSuspenseList', () => {
64
root.render(<App show={false} />);
65
expect(Scheduler).toFlushAndYield([]);
66
66
- root.render(<App show={true} />);
67
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
68
+ React.unstable_startTransition(() => {
69
+ root.render(<App show={true} />);
70
+ });
71
+ } else {
72
+ root.render(<App show={true} />);
73
+ }
74
expect(Scheduler).toFlushAndYield([
75
'Suspend! [A]',
76
'Suspend! [B]',
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+109
-16
@@ -111,9 +111,15 @@ describe('ReactExpiration', () => {
111
// Flush the sync task.
112
ReactNoop.flushSync();
113
}
114
-
114
+ // @gate experimental || !enableSyncDefaultUpdates
115
it('increases priority of updates as time progresses', () => {
116
- ReactNoop.render(<span prop="done" />);
116
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
117
+ React.unstable_startTransition(() => {
118
+ ReactNoop.render(<span prop="done" />);
119
+ });
120
+ } else {
121
+ ReactNoop.render(<span prop="done" />);
122
+ }
123
124
expect(ReactNoop.getChildren()).toEqual([]);
125
@@ -132,6 +138,7 @@ describe('ReactExpiration', () => {
138
expect(ReactNoop.getChildren()).toEqual([span('done')]);
139
});
140
141
+ // @gate experimental || !enableSyncDefaultUpdates
142
it('two updates of like priority in the same event always flush within the same batch', () => {
143
class TextClass extends React.Component {
144
componentDidMount() {
@@ -154,7 +161,13 @@ describe('ReactExpiration', () => {
161
162
// First, show what happens for updates in two separate events.
163
// Schedule an update.
157
- ReactNoop.render(<TextClass text="A" />);
164
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
165
+ React.unstable_startTransition(() => {
166
+ ReactNoop.render(<TextClass text="A" />);
167
+ });
168
+ } else {
169
+ ReactNoop.render(<TextClass text="A" />);
170
+ }
171
// Advance the timer.
172
Scheduler.unstable_advanceTime(2000);
173
// Partially flush the first update, then interrupt it.
@@ -184,6 +197,7 @@ describe('ReactExpiration', () => {
197
expect(Scheduler).toFlushAndYield(['B [render]', 'B [commit]']);
198
});
199
200
+ // @gate experimental || !enableSyncDefaultUpdates
201
it(
202
'two updates of like priority in the same event always flush within the ' +
203
"same batch, even if there's a sync update in between",
@@ -209,7 +223,13 @@ describe('ReactExpiration', () => {
223
224
// First, show what happens for updates in two separate events.
225
// Schedule an update.
212
- ReactNoop.render(<TextClass text="A" />);
226
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
227
+ React.unstable_startTransition(() => {
228
+ ReactNoop.render(<TextClass text="A" />);
229
+ });
230
+ } else {
231
+ ReactNoop.render(<TextClass text="A" />);
232
+ }
233
// Advance the timer.
234
Scheduler.unstable_advanceTime(2000);
235
// Partially flush the first update, then interrupt it.
@@ -245,6 +265,7 @@ describe('ReactExpiration', () => {
265
},
266
);
267
268
+ // @gate experimental || !enableSyncDefaultUpdates
269
it('cannot update at the same expiration time that is already rendering', () => {
270
const store = {text: 'initial'};
271
const subscribers = [];
@@ -281,7 +302,13 @@ describe('ReactExpiration', () => {
302
}
303
304
// Initial mount
284
- ReactNoop.render(<App />);
305
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
306
+ React.unstable_startTransition(() => {
307
+ ReactNoop.render(<App />);
308
+ });
309
+ } else {
310
+ ReactNoop.render(<App />);
311
+ }
312
expect(Scheduler).toFlushAndYield([
313
'initial [A] [render]',
314
'initial [B] [render]',
@@ -294,7 +321,13 @@ describe('ReactExpiration', () => {
321
]);
322
323
// Partial update
297
- subscribers.forEach(s => s.setState({text: '1'}));
324
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
325
+ React.unstable_startTransition(() => {
326
+ subscribers.forEach(s => s.setState({text: '1'}));
327
+ });
328
+ } else {
329
+ subscribers.forEach(s => s.setState({text: '1'}));
330
+ }
331
expect(Scheduler).toFlushAndYieldThrough([
332
'1 [A] [render]',
333
'1 [B] [render]',
@@ -310,6 +343,7 @@ describe('ReactExpiration', () => {
343
]);
344
});
345
346
+ // @gate experimental || !enableSyncDefaultUpdates
347
it('stops yielding if CPU-bound update takes too long to finish', () => {
348
const root = ReactNoop.createRoot();
349
function App() {
@@ -324,7 +358,13 @@ describe('ReactExpiration', () => {
358
);
359
}
360
327
- root.render(<App />);
361
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
362
+ React.unstable_startTransition(() => {
363
+ root.render(<App />);
364
+ });
365
+ } else {
366
+ root.render(<App />);
367
+ }
368
369
expect(Scheduler).toFlushAndYieldThrough(['A']);
370
expect(Scheduler).toFlushAndYieldThrough(['B']);
@@ -337,6 +377,7 @@ describe('ReactExpiration', () => {
377
expect(root).toMatchRenderedOutput('ABCDE');
378
});
379
380
+ // @gate experimental || !enableSyncDefaultUpdates
381
it('root expiration is measured from the time of the first update', () => {
382
Scheduler.unstable_advanceTime(10000);
383
@@ -352,8 +393,13 @@ describe('ReactExpiration', () => {
393
</>
394
);
395
}
355
-
356
- root.render(<App />);
396
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
397
+ React.unstable_startTransition(() => {
398
+ root.render(<App />);
399
+ });
400
+ } else {
401
+ root.render(<App />);
402
+ }
403
404
expect(Scheduler).toFlushAndYieldThrough(['A']);
405
expect(Scheduler).toFlushAndYieldThrough(['B']);
@@ -366,6 +412,7 @@ describe('ReactExpiration', () => {
412
expect(root).toMatchRenderedOutput('ABCDE');
413
});
414
415
+ // @gate experimental || !enableSyncDefaultUpdates
416
it('should measure expiration times relative to module initialization', () => {
417
// Tests an implementation detail where expiration times are computed using
418
// bitwise operations.
@@ -381,12 +428,24 @@ describe('ReactExpiration', () => {
428
// current time.
429
ReactNoop = require('react-noop-renderer');
430
384
- ReactNoop.render('Hi');
431
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
432
+ React.unstable_startTransition(() => {
433
+ ReactNoop.render('Hi');
434
+ });
435
+ } else {
436
+ ReactNoop.render('Hi');
437
+ }
438
439
// The update should not have expired yet.
440
flushNextRenderIfExpired();
441
expect(Scheduler).toHaveYielded([]);
389
- expect(ReactNoop).toMatchRenderedOutput(null);
442
+
443
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
444
+ // TODO: Why is this flushed?
445
+ expect(ReactNoop).toMatchRenderedOutput('Hi');
446
+ } else {
447
+ expect(ReactNoop).toMatchRenderedOutput(null);
448
+ }
449
450
// Advance the time some more to expire the update.
451
Scheduler.unstable_advanceTime(10000);
@@ -395,6 +454,7 @@ describe('ReactExpiration', () => {
454
expect(ReactNoop).toMatchRenderedOutput('Hi');
455
});
456
457
+ // @gate experimental || !enableSyncDefaultUpdates
458
it('should measure callback timeout relative to current time, not start-up time', () => {
459
// Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
460
// The bug wasn't caught by other tests because we use virtual times that
@@ -403,7 +463,13 @@ describe('ReactExpiration', () => {
463
// Before scheduling an update, advance the current time.
464
Scheduler.unstable_advanceTime(10000);
465
406
- ReactNoop.render('Hi');
466
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
467
+ React.unstable_startTransition(() => {
468
+ ReactNoop.render('Hi');
469
+ });
470
+ } else {
471
+ ReactNoop.render('Hi');
472
+ }
473
flushNextRenderIfExpired();
474
expect(Scheduler).toHaveYielded([]);
475
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -418,6 +484,7 @@ describe('ReactExpiration', () => {
484
expect(ReactNoop).toMatchRenderedOutput('Hi');
485
});
486
487
+ // @gate experimental || !enableSyncDefaultUpdates
488
it('prevents starvation by sync updates', async () => {
489
const {useState} = React;
490
@@ -450,7 +517,13 @@ describe('ReactExpiration', () => {
517
518
// First demonstrate what happens when there's no starvation
519
await ReactNoop.act(async () => {
453
- updateNormalPri();
520
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
521
+ React.unstable_startTransition(() => {
522
+ updateNormalPri();
523
+ });
524
+ } else {
525
+ updateNormalPri();
526
+ }
527
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 0']);
528
updateSyncPri();
529
});
@@ -466,7 +539,13 @@ describe('ReactExpiration', () => {
539
540
// Do the same thing, but starve the first update
541
await ReactNoop.act(async () => {
469
- updateNormalPri();
542
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
543
+ React.unstable_startTransition(() => {
544
+ updateNormalPri();
545
+ });
546
+ } else {
547
+ updateNormalPri();
548
+ }
549
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 1']);
550
551
// This time, a lot of time has elapsed since the normal pri update
@@ -628,6 +707,7 @@ describe('ReactExpiration', () => {
707
});
708
});
709
710
+ // @gate experimental || !enableSyncDefaultUpdates
711
it('detects starvation in multiple batches', async () => {
712
const {useState} = React;
713
@@ -666,7 +746,13 @@ describe('ReactExpiration', () => {
746
747
await ReactNoop.act(async () => {
748
// Partially render an update
669
- updateNormalPri();
749
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
750
+ React.unstable_startTransition(() => {
751
+ updateNormalPri();
752
+ });
753
+ } else {
754
+ updateNormalPri();
755
+ }
756
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
757
// Some time goes by. In an interleaved event, schedule another update.
758
// This will be placed into a separate batch.
@@ -693,6 +779,7 @@ describe('ReactExpiration', () => {
779
});
780
});
781
782
+ // @gate experimental || !enableSyncDefaultUpdates
783
it('updates do not expire while they are IO-bound', async () => {
784
const {Suspense} = React;
785
@@ -715,7 +802,13 @@ describe('ReactExpiration', () => {
802
expect(root).toMatchRenderedOutput('A, Sibling');
803
804
await ReactNoop.act(async () => {
718
- root.render(<App text="B" />);
805
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
806
+ React.unstable_startTransition(() => {
807
+ root.render(<App text="B" />);
808
+ });
809
+ } else {
810
+ root.render(<App text="B" />);
811
+ }
812
expect(Scheduler).toFlushAndYield([
813
'Suspend! [B]',
814
'Sibling',
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+21
-5
@@ -22,6 +22,7 @@ describe('ReactFlushSync', () => {
22
return text;
23
}
24
25
+ // @gate experimental || !enableSyncDefaultUpdates
26
test('changes priority of updates in useEffect', async () => {
27
function App() {
28
const [syncState, setSyncState] = useState(0);
@@ -37,22 +38,37 @@ describe('ReactFlushSync', () => {
38
39
const root = ReactNoop.createRoot();
40
await ReactNoop.act(async () => {
40
- root.render(<App />);
41
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
42
+ React.unstable_startTransition(() => {
43
+ root.render(<App />);
44
+ });
45
+ } else {
46
+ root.render(<App />);
47
+ }
48
// This will yield right before the passive effect fires
49
expect(Scheduler).toFlushUntilNextPaint(['0, 0']);
50
51
// The passive effect will schedule a sync update and a normal update.
52
// They should commit in two separate batches. First the sync one.
46
- expect(() =>
47
- expect(Scheduler).toFlushUntilNextPaint(['1, 0']),
48
- ).toErrorDev('flushSync was called from inside a lifecycle method');
53
+ expect(() => {
54
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
55
+ expect(Scheduler).toFlushUntilNextPaint(['1, 0', '1, 1']);
56
+ } else {
57
+ expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
58
+ }
59
+ }).toErrorDev('flushSync was called from inside a lifecycle method');
60
61
// The remaining update is not sync
62
ReactNoop.flushSync();
63
expect(Scheduler).toHaveYielded([]);
64
65
// Now flush it.
55
- expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
66
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
67
+ // With sync default updates, passive effects are synchronously flushed.
68
+ expect(Scheduler).toHaveYielded([]);
69
+ } else {
70
+ expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
71
+ }
72
});
73
expect(root).toMatchRenderedOutput('1, 1');
74
});
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+108
-24
@@ -151,6 +151,7 @@ describe('ReactHooksWithNoopRenderer', () => {
151
return Promise.resolve().then(() => {});
152
}
153
154
+ // @gate experimental || !enableSyncDefaultUpdates
155
it('resumes after an interruption', () => {
156
function Counter(props, ref) {
157
const [count, updateCount] = useState(0);
@@ -166,10 +167,20 @@ describe('ReactHooksWithNoopRenderer', () => {
167
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
168
169
// Schedule some updates
169
- ReactNoop.batchedUpdates(() => {
170
- counter.current.updateCount(1);
171
- counter.current.updateCount(count => count + 10);
172
- });
170
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
171
+ React.unstable_startTransition(() => {
172
+ // TODO: Batched updates need to be inside startTransition?
173
+ ReactNoop.batchedUpdates(() => {
174
+ counter.current.updateCount(1);
175
+ counter.current.updateCount(count => count + 10);
176
+ });
177
+ });
178
+ } else {
179
+ ReactNoop.batchedUpdates(() => {
180
+ counter.current.updateCount(1);
181
+ counter.current.updateCount(count => count + 10);
182
+ });
183
+ }
184
185
// Partially flush without committing
186
expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
@@ -690,6 +701,7 @@ describe('ReactHooksWithNoopRenderer', () => {
701
expect(ReactNoop.getChildren()).toEqual([span(22)]);
702
});
703
704
+ // @gate experimental || !enableSyncDefaultUpdates
705
it('discards render phase updates if something suspends', async () => {
706
const thenable = {then() {}};
707
function Foo({signal}) {
@@ -725,15 +737,28 @@ describe('ReactHooksWithNoopRenderer', () => {
737
expect(Scheduler).toFlushAndYield([0]);
738
expect(root).toMatchRenderedOutput(<span prop={0} />);
739
728
- root.render(<Foo signal={false} />);
740
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
741
+ React.unstable_startTransition(() => {
742
+ root.render(<Foo signal={false} />);
743
+ });
744
+ } else {
745
+ root.render(<Foo signal={false} />);
746
+ }
747
expect(Scheduler).toFlushAndYield(['Suspend!']);
748
expect(root).toMatchRenderedOutput(<span prop={0} />);
749
750
// Rendering again should suspend again.
733
- root.render(<Foo signal={false} />);
751
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
752
+ React.unstable_startTransition(() => {
753
+ root.render(<Foo signal={false} />);
754
+ });
755
+ } else {
756
+ root.render(<Foo signal={false} />);
757
+ }
758
expect(Scheduler).toFlushAndYield(['Suspend!']);
759
});
760
761
+ // @gate experimental || !enableSyncDefaultUpdates
762
it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
763
const thenable = {then() {}};
764
function Foo({signal}) {
@@ -776,19 +801,38 @@ describe('ReactHooksWithNoopRenderer', () => {
801
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
802
803
await ReactNoop.act(async () => {
779
- root.render(<Foo signal={false} />);
780
- setLabel('B');
804
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
805
+ React.unstable_startTransition(() => {
806
+ root.render(<Foo signal={false} />);
807
+ setLabel('B');
808
+ });
809
+ } else {
810
+ root.render(<Foo signal={false} />);
811
+ setLabel('B');
812
+ }
813
814
expect(Scheduler).toFlushAndYield(['Suspend!']);
815
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
816
817
// Rendering again should suspend again.
786
- root.render(<Foo signal={false} />);
818
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
819
+ React.unstable_startTransition(() => {
820
+ root.render(<Foo signal={false} />);
821
+ });
822
+ } else {
823
+ root.render(<Foo signal={false} />);
824
+ }
825
expect(Scheduler).toFlushAndYield(['Suspend!']);
826
827
// Flip the signal back to "cancel" the update. However, the update to
828
// label should still proceed. It shouldn't have been dropped.
791
- root.render(<Foo signal={true} />);
829
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
830
+ React.unstable_startTransition(() => {
831
+ root.render(<Foo signal={true} />);
832
+ });
833
+ } else {
834
+ root.render(<Foo signal={true} />);
835
+ }
836
expect(Scheduler).toFlushAndYield(['B:0']);
837
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
838
});
@@ -1284,6 +1328,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1328
});
1329
});
1330
1331
+ // @gate experimental || !enableSyncDefaultUpdates
1332
it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => {
1333
let setParentState = null;
1334
const setChildStates = [];
@@ -1350,17 +1395,34 @@ describe('ReactHooksWithNoopRenderer', () => {
1395
]);
1396
1397
// Schedule another update for children, and partially process it.
1353
- setChildStates.forEach(setChildState => setChildState(2));
1398
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1399
+ React.unstable_startTransition(() => {
1400
+ setChildStates.forEach(setChildState => setChildState(2));
1401
+ });
1402
+ } else {
1403
+ setChildStates.forEach(setChildState => setChildState(2));
1404
+ }
1405
expect(Scheduler).toFlushAndYieldThrough(['Child one render']);
1406
1407
// Schedule unmount for the parent that unmounts children with pending update.
1408
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
1409
setParentState(false);
1410
});
1360
- expect(Scheduler).toFlushUntilNextPaint([
1361
- 'Parent false render',
1362
- 'Parent false commit',
1363
- ]);
1411
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1412
+ expect(Scheduler).toFlushUntilNextPaint([
1413
+ // TODO: why do the children render and fire effects?
1414
+ 'Child two render',
1415
+ 'Child one commit',
1416
+ 'Child two commit',
1417
+ 'Parent false render',
1418
+ 'Parent false commit',
1419
+ ]);
1420
+ } else {
1421
+ expect(Scheduler).toFlushUntilNextPaint([
1422
+ 'Parent false render',
1423
+ 'Parent false commit',
1424
+ ]);
1425
+ }
1426
1427
// Schedule updates for children too (which should be ignored)
1428
setChildStates.forEach(setChildState => setChildState(2));
@@ -1647,6 +1709,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1709
});
1710
});
1711
1712
+ // @gate experimental || !enableSyncDefaultUpdates
1713
it('updates have async priority even if effects are flushed early', () => {
1714
function Counter(props) {
1715
const [count, updateCount] = useState('(empty)');
@@ -1667,20 +1730,41 @@ describe('ReactHooksWithNoopRenderer', () => {
1730
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1731
1732
// Rendering again should flush the previous commit's effects
1670
- ReactNoop.render(<Counter count={1} />, () =>
1671
- Scheduler.unstable_yieldValue('Sync effect'),
1672
- );
1733
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1734
+ React.unstable_startTransition(() => {
1735
+ ReactNoop.render(<Counter count={1} />, () =>
1736
+ Scheduler.unstable_yieldValue('Sync effect'),
1737
+ );
1738
+ });
1739
+ } else {
1740
+ ReactNoop.render(<Counter count={1} />, () =>
1741
+ Scheduler.unstable_yieldValue('Sync effect'),
1742
+ );
1743
+ }
1744
+
1745
expect(Scheduler).toFlushAndYieldThrough([
1746
'Schedule update [0]',
1747
'Count: 0',
1748
]);
1677
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1749
1679
- expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1680
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1681
- ReactNoop.flushPassiveEffects();
1682
- expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1683
- expect(Scheduler).toFlushAndYield(['Count: 1']);
1750
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1751
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1752
+ expect(Scheduler).toFlushAndYieldThrough([
1753
+ 'Count: 0',
1754
+ 'Sync effect',
1755
+ 'Schedule update [1]',
1756
+ 'Count: 1',
1757
+ ]);
1758
+ } else {
1759
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1760
+ expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1761
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1762
+
1763
+ ReactNoop.flushPassiveEffects();
1764
+ expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1765
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
1766
+ }
1767
+
1768
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1769
});
1770
});
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+140
-39
@@ -50,6 +50,7 @@ describe('ReactIncremental', () => {
50
expect(Scheduler).toFlushWithoutYielding();
51
});
52
53
+ // @gate experimental || !enableSyncDefaultUpdates
54
it('should render a simple component, in steps if needed', () => {
55
function Bar() {
56
Scheduler.unstable_yieldValue('Bar');
@@ -65,7 +66,17 @@ describe('ReactIncremental', () => {
66
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
67
}
68
68
- ReactNoop.render(<Foo />, () => Scheduler.unstable_yieldValue('callback'));
69
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
70
+ React.unstable_startTransition(() => {
71
+ ReactNoop.render(<Foo />, () =>
72
+ Scheduler.unstable_yieldValue('callback'),
73
+ );
74
+ });
75
+ } else {
76
+ ReactNoop.render(<Foo />, () =>
77
+ Scheduler.unstable_yieldValue('callback'),
78
+ );
79
+ }
80
// Do one step of work.
81
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
82
@@ -132,6 +143,7 @@ describe('ReactIncremental', () => {
143
]);
144
});
145
146
+ // @gate experimental || !enableSyncDefaultUpdates
147
it('can cancel partially rendered work and restart', () => {
148
function Bar(props) {
149
Scheduler.unstable_yieldValue('Bar');
@@ -152,13 +164,26 @@ describe('ReactIncremental', () => {
164
ReactNoop.render(<Foo text="foo" />);
165
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
166
155
- ReactNoop.render(<Foo text="bar" />);
167
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
168
+ React.unstable_startTransition(() => {
169
+ ReactNoop.render(<Foo text="bar" />);
170
+ });
171
+ } else {
172
+ ReactNoop.render(<Foo text="bar" />);
173
+ }
174
// Flush part of the work
175
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
176
177
// This will abort the previous work and restart
178
ReactNoop.flushSync(() => ReactNoop.render(null));
161
- ReactNoop.render(<Foo text="baz" />);
179
+
180
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
181
+ React.unstable_startTransition(() => {
182
+ ReactNoop.render(<Foo text="baz" />);
183
+ });
184
+ } else {
185
+ ReactNoop.render(<Foo text="baz" />);
186
+ }
187
188
// Flush part of the new work
189
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
@@ -167,6 +192,7 @@ describe('ReactIncremental', () => {
192
expect(Scheduler).toFlushAndYield(['Bar']);
193
});
194
195
+ // @gate experimental || !enableSyncDefaultUpdates
196
it('should call callbacks even if updates are aborted', () => {
197
let inst;
198
@@ -192,26 +218,50 @@ describe('ReactIncremental', () => {
218
ReactNoop.render(<Foo />);
219
expect(Scheduler).toFlushWithoutYielding();
220
195
- inst.setState(
196
- () => {
197
- Scheduler.unstable_yieldValue('setState1');
198
- return {text: 'bar'};
199
- },
200
- () => Scheduler.unstable_yieldValue('callback1'),
201
- );
221
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
222
+ React.unstable_startTransition(() => {
223
+ inst.setState(
224
+ () => {
225
+ Scheduler.unstable_yieldValue('setState1');
226
+ return {text: 'bar'};
227
+ },
228
+ () => Scheduler.unstable_yieldValue('callback1'),
229
+ );
230
+ });
231
+ } else {
232
+ inst.setState(
233
+ () => {
234
+ Scheduler.unstable_yieldValue('setState1');
235
+ return {text: 'bar'};
236
+ },
237
+ () => Scheduler.unstable_yieldValue('callback1'),
238
+ );
239
+ }
240
241
// Flush part of the work
242
expect(Scheduler).toFlushAndYieldThrough(['setState1']);
243
244
// This will abort the previous work and restart
245
ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
208
- inst.setState(
209
- () => {
210
- Scheduler.unstable_yieldValue('setState2');
211
- return {text2: 'baz'};
212
- },
213
- () => Scheduler.unstable_yieldValue('callback2'),
214
- );
246
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
247
+ React.unstable_startTransition(() => {
248
+ inst.setState(
249
+ () => {
250
+ Scheduler.unstable_yieldValue('setState2');
251
+ return {text2: 'baz'};
252
+ },
253
+ () => Scheduler.unstable_yieldValue('callback2'),
254
+ );
255
+ });
256
+ } else {
257
+ inst.setState(
258
+ () => {
259
+ Scheduler.unstable_yieldValue('setState2');
260
+ return {text2: 'baz'};
261
+ },
262
+ () => Scheduler.unstable_yieldValue('callback2'),
263
+ );
264
+ }
265
266
// Flush the rest of the work which now includes the low priority
267
expect(Scheduler).toFlushAndYield([
@@ -1714,6 +1764,7 @@ describe('ReactIncremental', () => {
1764
expect(instance.state.n).toEqual(3);
1765
});
1766
1767
+ // @gate experimental || !enableSyncDefaultUpdates
1768
it('merges and masks context', () => {
1769
class Intl extends React.Component {
1770
static childContextTypes = {
@@ -1838,15 +1889,27 @@ describe('ReactIncremental', () => {
1889
'ShowLocale {"locale":"de"}',
1890
'ShowBoth {"locale":"de"}',
1891
]);
1841
-
1842
- ReactNoop.render(
1843
- <Intl locale="sv">
1844
- <ShowLocale />
1845
- <div>
1846
- <ShowBoth />
1847
- </div>
1848
- </Intl>,
1849
- );
1892
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1893
+ React.unstable_startTransition(() => {
1894
+ ReactNoop.render(
1895
+ <Intl locale="sv">
1896
+ <ShowLocale />
1897
+ <div>
1898
+ <ShowBoth />
1899
+ </div>
1900
+ </Intl>,
1901
+ );
1902
+ });
1903
+ } else {
1904
+ ReactNoop.render(
1905
+ <Intl locale="sv">
1906
+ <ShowLocale />
1907
+ <div>
1908
+ <ShowBoth />
1909
+ </div>
1910
+ </Intl>,
1911
+ );
1912
+ }
1913
expect(Scheduler).toFlushAndYieldThrough(['Intl {}']);
1914
1915
ReactNoop.render(
@@ -1994,18 +2057,35 @@ describe('ReactIncremental', () => {
2057
}
2058
}
2059
1997
- ReactNoop.render(
1998
- <Intl locale="fr">
1999
- <ShowLocale />
2000
- <LegacyHiddenDiv mode="hidden">
2060
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2061
+ React.unstable_startTransition(() => {
2062
+ ReactNoop.render(
2063
+ <Intl locale="fr">
2064
+ <ShowLocale />
2065
+ <LegacyHiddenDiv mode="hidden">
2066
+ <ShowLocale />
2067
+ <Intl locale="ru">
2068
+ <ShowLocale />
2069
+ </Intl>
2070
+ </LegacyHiddenDiv>
2071
+ <ShowLocale />
2072
+ </Intl>,
2073
+ );
2074
+ });
2075
+ } else {
2076
+ ReactNoop.render(
2077
+ <Intl locale="fr">
2078
<ShowLocale />
2002
- <Intl locale="ru">
2079
+ <LegacyHiddenDiv mode="hidden">
2080
<ShowLocale />
2004
- </Intl>
2005
- </LegacyHiddenDiv>
2006
- <ShowLocale />
2007
- </Intl>,
2008
- );
2081
+ <Intl locale="ru">
2082
+ <ShowLocale />
2083
+ </Intl>
2084
+ </LegacyHiddenDiv>
2085
+ <ShowLocale />
2086
+ </Intl>,
2087
+ );
2088
+ }
2089
expect(Scheduler).toFlushAndYieldThrough([
2090
'Intl {}',
2091
'ShowLocale {"locale":"fr"}',
@@ -2748,6 +2828,7 @@ describe('ReactIncremental', () => {
2828
expect(Scheduler).toFlushAndYield(['count:1, name:not brian']);
2829
});
2830
2831
+ // @gate experimental || !enableSyncDefaultUpdates
2832
it('does not interrupt for update at same priority', () => {
2833
function Parent(props) {
2834
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2759,7 +2840,13 @@ describe('ReactIncremental', () => {
2840
return null;
2841
}
2842
2762
- ReactNoop.render(<Parent step={1} />);
2843
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2844
+ React.unstable_startTransition(() => {
2845
+ ReactNoop.render(<Parent step={1} />);
2846
+ });
2847
+ } else {
2848
+ ReactNoop.render(<Parent step={1} />);
2849
+ }
2850
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
2851
2852
// Interrupt at same priority
@@ -2768,6 +2855,7 @@ describe('ReactIncremental', () => {
2855
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
2856
});
2857
2858
+ // @gate experimental || !enableSyncDefaultUpdates
2859
it('does not interrupt for update at lower priority', () => {
2860
function Parent(props) {
2861
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2779,7 +2867,13 @@ describe('ReactIncremental', () => {
2867
return null;
2868
}
2869
2782
- ReactNoop.render(<Parent step={1} />);
2870
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2871
+ React.unstable_startTransition(() => {
2872
+ ReactNoop.render(<Parent step={1} />);
2873
+ });
2874
+ } else {
2875
+ ReactNoop.render(<Parent step={1} />);
2876
+ }
2877
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
2878
2879
// Interrupt at lower priority
@@ -2789,6 +2883,7 @@ describe('ReactIncremental', () => {
2883
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
2884
});
2885
2886
+ // @gate experimental || !enableSyncDefaultUpdates
2887
it('does interrupt for update at higher priority', () => {
2888
function Parent(props) {
2889
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2800,7 +2895,13 @@ describe('ReactIncremental', () => {
2895
return null;
2896
}
2897
2803
- ReactNoop.render(<Parent step={1} />);
2898
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2899
+ React.unstable_startTransition(() => {
2900
+ ReactNoop.render(<Parent step={1} />);
2901
+ });
2902
+ } else {
2903
+ ReactNoop.render(<Parent step={1} />);
2904
+ }
2905
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
2906
2907
// Interrupt at higher priority
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+121
-59
@@ -58,6 +58,7 @@ describe('ReactIncrementalErrorHandling', () => {
58
);
59
}
60
61
+ // @gate experimental || !enableSyncDefaultUpdates
62
it('recovers from errors asynchronously', () => {
63
class ErrorBoundary extends React.Component {
64
state = {error: null};
@@ -90,19 +91,37 @@ describe('ReactIncrementalErrorHandling', () => {
91
throw new Error('oops!');
92
}
93
93
- ReactNoop.render(
94
- <ErrorBoundary>
95
- <Indirection>
94
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
95
+ React.unstable_startTransition(() => {
96
+ ReactNoop.render(
97
+ <ErrorBoundary>
98
+ <Indirection>
99
+ <Indirection>
100
+ <Indirection>
101
+ <BadRender />
102
+ <Indirection />
103
+ <Indirection />
104
+ </Indirection>
105
+ </Indirection>
106
+ </Indirection>
107
+ </ErrorBoundary>,
108
+ );
109
+ });
110
+ } else {
111
+ ReactNoop.render(
112
+ <ErrorBoundary>
113
<Indirection>
114
<Indirection>
98
- <BadRender />
99
- <Indirection />
100
- <Indirection />
115
+ <Indirection>
116
+ <BadRender />
117
+ <Indirection />
118
+ <Indirection />
119
+ </Indirection>
120
</Indirection>
121
</Indirection>
103
- </Indirection>
104
- </ErrorBoundary>,
105
- );
122
+ </ErrorBoundary>,
123
+ );
124
+ }
125
126
// Start rendering asynchronously
127
expect(Scheduler).toFlushAndYieldThrough([
@@ -152,6 +171,7 @@ describe('ReactIncrementalErrorHandling', () => {
171
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
172
});
173
174
+ // @gate experimental || !enableSyncDefaultUpdates
175
it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
176
class ErrorBoundary extends React.Component {
177
state = {error: null};
@@ -184,19 +204,37 @@ describe('ReactIncrementalErrorHandling', () => {
204
throw new Error('oops!');
205
}
206
187
- ReactNoop.render(
188
- <ErrorBoundary>
189
- <Indirection>
207
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
208
+ React.unstable_startTransition(() => {
209
+ ReactNoop.render(
210
+ <ErrorBoundary>
211
+ <Indirection>
212
+ <Indirection>
213
+ <Indirection>
214
+ <BadRender />
215
+ <Indirection />
216
+ <Indirection />
217
+ </Indirection>
218
+ </Indirection>
219
+ </Indirection>
220
+ </ErrorBoundary>,
221
+ );
222
+ });
223
+ } else {
224
+ ReactNoop.render(
225
+ <ErrorBoundary>
226
<Indirection>
227
<Indirection>
192
- <BadRender />
193
- <Indirection />
194
- <Indirection />
228
+ <Indirection>
229
+ <BadRender />
230
+ <Indirection />
231
+ <Indirection />
232
+ </Indirection>
233
</Indirection>
234
</Indirection>
197
- </Indirection>
198
- </ErrorBoundary>,
199
- );
235
+ </ErrorBoundary>,
236
+ );
237
+ }
238
239
// Start rendering asynchronously
240
expect(Scheduler).toFlushAndYieldThrough([
@@ -247,27 +285,16 @@ describe('ReactIncrementalErrorHandling', () => {
285
Scheduler.unstable_yieldValue('commit');
286
}
287
250
- function interrupt() {
251
- ReactNoop.flushSync(() => {
252
- ReactNoop.renderToRootWithID(null, 'other-root');
253
- });
254
- }
255
-
256
- ReactNoop.render(<App isBroken={true} />, onCommit);
288
+ React.unstable_startTransition(() => {
289
+ ReactNoop.render(<App isBroken={true} />, onCommit);
290
+ });
291
expect(Scheduler).toFlushAndYieldThrough(['error']);
258
- interrupt();
292
293
React.unstable_startTransition(() => {
294
// This update is in a separate batch
295
ReactNoop.render(<App isBroken={false} />, onCommit);
296
});
297
265
- expect(Scheduler).toFlushAndYieldThrough([
266
- // The first render fails. But because there's a lower priority pending
267
- // update, it doesn't throw.
268
- 'error',
269
- ]);
270
-
298
// React will try to recover by rendering all the pending updates in a
299
// single batch, synchronously. This time it succeeds.
300
//
@@ -306,15 +333,10 @@ describe('ReactIncrementalErrorHandling', () => {
333
Scheduler.unstable_yieldValue('commit');
334
}
335
309
- function interrupt() {
310
- ReactNoop.flushSync(() => {
311
- ReactNoop.renderToRootWithID(null, 'other-root');
312
- });
313
- }
314
-
315
- ReactNoop.render(<App isBroken={true} />, onCommit);
336
+ React.unstable_startTransition(() => {
337
+ ReactNoop.render(<App isBroken={true} />, onCommit);
338
+ });
339
expect(Scheduler).toFlushAndYieldThrough(['error']);
317
- interrupt();
340
341
expect(ReactNoop).toMatchRenderedOutput(null);
342
@@ -323,12 +345,6 @@ describe('ReactIncrementalErrorHandling', () => {
345
ReactNoop.render(<App isBroken={false} />, onCommit);
346
});
347
326
- expect(Scheduler).toFlushAndYieldThrough([
327
- // The first render fails. But because there's a lower priority pending
328
- // update, it doesn't throw.
329
- 'error',
330
- ]);
331
-
348
// React will try to recover by rendering all the pending updates in a
349
// single batch, synchronously. This time it succeeds.
350
//
@@ -362,6 +378,7 @@ describe('ReactIncrementalErrorHandling', () => {
378
);
379
});
380
381
+ // @gate experimental || !enableSyncDefaultUpdates
382
it('retries one more time before handling error', () => {
383
function BadRender({unused}) {
384
Scheduler.unstable_yieldValue('BadRender');
@@ -383,7 +400,17 @@ describe('ReactIncrementalErrorHandling', () => {
400
);
401
}
402
386
- ReactNoop.render(<Parent />, () => Scheduler.unstable_yieldValue('commit'));
403
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
404
+ React.unstable_startTransition(() => {
405
+ ReactNoop.render(<Parent />, () =>
406
+ Scheduler.unstable_yieldValue('commit'),
407
+ );
408
+ });
409
+ } else {
410
+ ReactNoop.render(<Parent />, () =>
411
+ Scheduler.unstable_yieldValue('commit'),
412
+ );
413
+ }
414
415
// Render the bad component asynchronously
416
expect(Scheduler).toFlushAndYieldThrough(['Parent', 'BadRender']);
@@ -402,6 +429,7 @@ describe('ReactIncrementalErrorHandling', () => {
429
expect(ReactNoop.getChildren()).toEqual([]);
430
});
431
432
+ // @gate experimental || !enableSyncDefaultUpdates
433
it('retries one more time if an error occurs during a render that expires midway through the tree', async () => {
434
function Oops({unused}) {
435
Scheduler.unstable_yieldValue('Oops');
@@ -425,7 +453,13 @@ describe('ReactIncrementalErrorHandling', () => {
453
);
454
}
455
428
- ReactNoop.render(<App />);
456
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
457
+ React.unstable_startTransition(() => {
458
+ ReactNoop.render(<App />);
459
+ });
460
+ } else {
461
+ ReactNoop.render(<App />);
462
+ }
463
464
// Render part of the tree
465
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
@@ -532,6 +566,7 @@ describe('ReactIncrementalErrorHandling', () => {
566
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
567
});
568
569
+ // @gate experimental || !enableSyncDefaultUpdates
570
it('catches render error in a boundary during partial deferred mounting', () => {
571
class ErrorBoundary extends React.Component {
572
state = {error: null};
@@ -556,11 +591,21 @@ describe('ReactIncrementalErrorHandling', () => {
591
throw new Error('Hello');
592
}
593
559
- ReactNoop.render(
560
- <ErrorBoundary>
561
- <BrokenRender />
562
- </ErrorBoundary>,
563
- );
594
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
595
+ React.unstable_startTransition(() => {
596
+ ReactNoop.render(
597
+ <ErrorBoundary>
598
+ <BrokenRender />
599
+ </ErrorBoundary>,
600
+ );
601
+ });
602
+ } else {
603
+ ReactNoop.render(
604
+ <ErrorBoundary>
605
+ <BrokenRender />
606
+ </ErrorBoundary>,
607
+ );
608
+ }
609
610
expect(Scheduler).toFlushAndYieldThrough(['ErrorBoundary render success']);
611
expect(ReactNoop.getChildren()).toEqual([]);
@@ -712,6 +757,7 @@ describe('ReactIncrementalErrorHandling', () => {
757
expect(ReactNoop.getChildren()).toEqual([]);
758
});
759
760
+ // @gate experimental || !enableSyncDefaultUpdates
761
it('propagates an error from a noop error boundary during partial deferred mounting', () => {
762
class RethrowErrorBoundary extends React.Component {
763
componentDidCatch(error) {
@@ -729,11 +775,21 @@ describe('ReactIncrementalErrorHandling', () => {
775
throw new Error('Hello');
776
}
777
732
- ReactNoop.render(
733
- <RethrowErrorBoundary>
734
- <BrokenRender />
735
- </RethrowErrorBoundary>,
736
- );
778
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
779
+ React.unstable_startTransition(() => {
780
+ ReactNoop.render(
781
+ <RethrowErrorBoundary>
782
+ <BrokenRender />
783
+ </RethrowErrorBoundary>,
784
+ );
785
+ });
786
+ } else {
787
+ ReactNoop.render(
788
+ <RethrowErrorBoundary>
789
+ <BrokenRender />
790
+ </RethrowErrorBoundary>,
791
+ );
792
+ }
793
794
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']);
795
@@ -1805,7 +1861,13 @@ describe('ReactIncrementalErrorHandling', () => {
1861
}
1862
1863
await ReactNoop.act(async () => {
1808
- root.render(<Oops />);
1864
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1865
+ React.unstable_startTransition(() => {
1866
+ root.render(<Oops />);
1867
+ });
1868
+ } else {
1869
+ root.render(<Oops />);
1870
+ }
1871
1872
// Render past the component that throws, then yield.
1873
expect(Scheduler).toFlushAndYieldThrough(['Oops']);
packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
+38
-5
@@ -34,6 +34,7 @@ describe('ReactIncrementalReflection', () => {
34
return {type: 'span', children: [], prop, hidden: false};
35
}
36
37
+ // @gate experimental || !enableSyncDefaultUpdates
38
it('handles isMounted even when the initial render is deferred', () => {
39
const instances = [];
40
@@ -63,7 +64,13 @@ describe('ReactIncrementalReflection', () => {
64
return <Component />;
65
}
66
66
- ReactNoop.render(<Foo />);
67
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
68
+ React.unstable_startTransition(() => {
69
+ ReactNoop.render(<Foo />);
70
+ });
71
+ } else {
72
+ ReactNoop.render(<Foo />);
73
+ }
74
75
// Render part way through but don't yet commit the updates.
76
expect(Scheduler).toFlushAndYieldThrough(['componentWillMount: false']);
@@ -81,6 +88,7 @@ describe('ReactIncrementalReflection', () => {
88
expect(instances[0]._isMounted()).toBe(true);
89
});
90
91
+ // @gate experimental || !enableSyncDefaultUpdates
92
it('handles isMounted when an unmount is deferred', () => {
93
const instances = [];
94
@@ -121,7 +129,13 @@ describe('ReactIncrementalReflection', () => {
129
130
expect(instances[0]._isMounted()).toBe(true);
131
124
- ReactNoop.render(<Foo mount={false} />);
132
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
133
+ React.unstable_startTransition(() => {
134
+ ReactNoop.render(<Foo mount={false} />);
135
+ });
136
+ } else {
137
+ ReactNoop.render(<Foo mount={false} />);
138
+ }
139
// Render part way through but don't yet commit the updates so it is not
140
// fully unmounted yet.
141
expect(Scheduler).toFlushAndYieldThrough(['Other']);
@@ -134,6 +148,7 @@ describe('ReactIncrementalReflection', () => {
148
expect(instances[0]._isMounted()).toBe(false);
149
});
150
151
+ // @gate experimental || !enableSyncDefaultUpdates
152
it('finds no node before insertion and correct node before deletion', () => {
153
let classInstance = null;
154
@@ -204,7 +219,13 @@ describe('ReactIncrementalReflection', () => {
219
return [<Component key="a" step={props.step} />, <Sibling key="b" />];
220
}
221
207
- ReactNoop.render(<Foo step={0} />);
222
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
223
+ React.unstable_startTransition(() => {
224
+ ReactNoop.render(<Foo step={0} />);
225
+ });
226
+ } else {
227
+ ReactNoop.render(<Foo step={0} />);
228
+ }
229
// Flush past Component but don't complete rendering everything yet.
230
expect(Scheduler).toFlushAndYieldThrough([
231
['componentWillMount', null],
@@ -246,7 +267,13 @@ describe('ReactIncrementalReflection', () => {
267
268
// The next step will render a new host node but won't get committed yet.
269
// We expect this to mutate the original Fiber.
249
- ReactNoop.render(<Foo step={2} />);
270
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
271
+ React.unstable_startTransition(() => {
272
+ ReactNoop.render(<Foo step={2} />);
273
+ });
274
+ } else {
275
+ ReactNoop.render(<Foo step={2} />);
276
+ }
277
expect(Scheduler).toFlushAndYieldThrough([
278
['componentWillUpdate', hostSpan],
279
'render',
@@ -267,7 +294,13 @@ describe('ReactIncrementalReflection', () => {
294
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
295
296
// Render to null but don't commit it yet.
270
- ReactNoop.render(<Foo step={3} />);
297
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
298
+ React.unstable_startTransition(() => {
299
+ ReactNoop.render(<Foo step={3} />);
300
+ });
301
+ } else {
302
+ ReactNoop.render(<Foo step={3} />);
303
+ }
304
expect(Scheduler).toFlushAndYieldThrough([
305
['componentWillUpdate', hostDiv],
306
'render',
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+71
-17
@@ -85,6 +85,7 @@ describe('ReactIncrementalScheduling', () => {
85
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
86
});
87
88
+ // @gate experimental || !enableSyncDefaultUpdates
89
it('works on deferred roots in the order they were scheduled', () => {
90
const {useEffect} = React;
91
function Text({text}) {
@@ -107,8 +108,15 @@ describe('ReactIncrementalScheduling', () => {
108
109
// Schedule deferred work in the reverse order
110
ReactNoop.act(() => {
110
- ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
111
- ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
111
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
112
+ React.unstable_startTransition(() => {
113
+ ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
114
+ ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
115
+ });
116
+ } else {
117
+ ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
118
+ ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
119
+ }
120
// Ensure it starts in the order it was scheduled
121
expect(Scheduler).toFlushAndYieldThrough(['c:2']);
122
@@ -117,7 +125,13 @@ describe('ReactIncrementalScheduling', () => {
125
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
126
// Schedule last bit of work, it will get processed the last
127
120
- ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
128
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
129
+ React.unstable_startTransition(() => {
130
+ ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
131
+ });
132
+ } else {
133
+ ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
134
+ }
135
136
// Keep performing work in the order it was scheduled
137
expect(Scheduler).toFlushAndYieldThrough(['b:2']);
@@ -132,6 +146,7 @@ describe('ReactIncrementalScheduling', () => {
146
});
147
});
148
149
+ // @gate experimental || !enableSyncDefaultUpdates
150
it('schedules sync updates when inside componentDidMount/Update', () => {
151
let instance;
152
@@ -170,7 +185,13 @@ describe('ReactIncrementalScheduling', () => {
185
}
186
}
187
173
- ReactNoop.render(<Foo />);
188
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
189
+ React.unstable_startTransition(() => {
190
+ ReactNoop.render(<Foo />);
191
+ });
192
+ } else {
193
+ ReactNoop.render(<Foo />);
194
+ }
195
// Render without committing
196
expect(Scheduler).toFlushAndYieldThrough(['render: 0']);
197
@@ -184,7 +205,13 @@ describe('ReactIncrementalScheduling', () => {
205
'componentDidUpdate: 1',
206
]);
207
187
- instance.setState({tick: 2});
208
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
209
+ React.unstable_startTransition(() => {
210
+ instance.setState({tick: 2});
211
+ });
212
+ } else {
213
+ instance.setState({tick: 2});
214
+ }
215
expect(Scheduler).toFlushAndYieldThrough(['render: 2']);
216
expect(ReactNoop.flushNextYield()).toEqual([
217
'componentDidUpdate: 2',
@@ -197,6 +224,7 @@ describe('ReactIncrementalScheduling', () => {
224
]);
225
});
226
227
+ // @gate experimental || !enableSyncDefaultUpdates
228
it('can opt-in to async scheduling inside componentDidMount/Update', () => {
229
let instance;
230
class Foo extends React.Component {
@@ -255,20 +283,39 @@ describe('ReactIncrementalScheduling', () => {
283
284
// Increment the tick to 2. This will trigger an update inside cDU. Flush
285
// the first update without flushing the second one.
258
- instance.setState({tick: 2});
259
- expect(Scheduler).toFlushAndYieldThrough([
260
- 'render: 2',
261
- 'componentDidUpdate: 2',
262
- 'componentDidUpdate (before setState): 2',
263
- 'componentDidUpdate (after setState): 2',
264
- ]);
265
- expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
286
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
287
+ React.unstable_startTransition(() => {
288
+ instance.setState({tick: 2});
289
+ });
290
267
- // Now flush the cDU update.
268
- expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
269
- expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
291
+ // TODO: why does this flush sync?
292
+ expect(Scheduler).toFlushAndYieldThrough([
293
+ 'render: 2',
294
+ 'componentDidUpdate: 2',
295
+ 'componentDidUpdate (before setState): 2',
296
+ 'componentDidUpdate (after setState): 2',
297
+ 'render: 3',
298
+ 'componentDidUpdate: 3',
299
+ ]);
300
+ expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
301
+ } else {
302
+ instance.setState({tick: 2});
303
+
304
+ expect(Scheduler).toFlushAndYieldThrough([
305
+ 'render: 2',
306
+ 'componentDidUpdate: 2',
307
+ 'componentDidUpdate (before setState): 2',
308
+ 'componentDidUpdate (after setState): 2',
309
+ ]);
310
+ expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
311
+
312
+ // Now flush the cDU update.
313
+ expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
314
+ expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
315
+ }
316
});
317
318
+ // @gate experimental || !enableSyncDefaultUpdates
319
it('performs Task work even after time runs out', () => {
320
class Foo extends React.Component {
321
state = {step: 1};
@@ -286,7 +333,14 @@ describe('ReactIncrementalScheduling', () => {
333
return <span prop={this.state.step} />;
334
}
335
}
289
- ReactNoop.render(<Foo />);
336
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
337
+ React.unstable_startTransition(() => {
338
+ ReactNoop.render(<Foo />);
339
+ });
340
+ } else {
341
+ ReactNoop.render(<Foo />);
342
+ }
343
+
344
// This should be just enough to complete all the work, but not enough to
345
// commit it.
346
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+31
-4
@@ -384,6 +384,7 @@ describe('ReactIncrementalSideEffects', () => {
384
expect(ReactNoop.getChildren('portalContainer')).toEqual([]);
385
});
386
387
+ // @gate experimental || !enableSyncDefaultUpdates
388
it('does not update child nodes if a flush is aborted', () => {
389
function Bar(props) {
390
Scheduler.unstable_yieldValue('Bar');
@@ -409,7 +410,13 @@ describe('ReactIncrementalSideEffects', () => {
410
div(div(span('Hello'), span('Hello')), span('Yo')),
411
]);
412
412
- ReactNoop.render(<Foo text="World" />);
413
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
414
+ React.unstable_startTransition(() => {
415
+ ReactNoop.render(<Foo text="World" />);
416
+ });
417
+ } else {
418
+ ReactNoop.render(<Foo text="World" />);
419
+ }
420
421
// Flush some of the work without committing
422
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
@@ -633,12 +640,19 @@ describe('ReactIncrementalSideEffects', () => {
640
);
641
});
642
643
+ // @gate experimental || !enableSyncDefaultUpdates
644
it('can update a completed tree before it has a chance to commit', () => {
645
function Foo(props) {
646
Scheduler.unstable_yieldValue('Foo');
647
return <span prop={props.step} />;
648
}
641
- ReactNoop.render(<Foo step={1} />);
649
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
650
+ React.unstable_startTransition(() => {
651
+ ReactNoop.render(<Foo step={1} />);
652
+ });
653
+ } else {
654
+ ReactNoop.render(<Foo step={1} />);
655
+ }
656
// This should be just enough to complete the tree without committing it
657
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
658
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
@@ -647,13 +661,26 @@ describe('ReactIncrementalSideEffects', () => {
661
ReactNoop.flushNextYield();
662
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
663
650
- ReactNoop.render(<Foo step={2} />);
664
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
665
+ React.unstable_startTransition(() => {
666
+ ReactNoop.render(<Foo step={2} />);
667
+ });
668
+ } else {
669
+ ReactNoop.render(<Foo step={2} />);
670
+ }
671
// This should be just enough to complete the tree without committing it
672
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
673
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
674
// This time, before we commit the tree, we update the root component with
675
// new props
656
- ReactNoop.render(<Foo step={3} />);
676
+
677
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
678
+ React.unstable_startTransition(() => {
679
+ ReactNoop.render(<Foo step={3} />);
680
+ });
681
+ } else {
682
+ ReactNoop.render(<Foo step={3} />);
683
+ }
684
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
685
// Now let's commit. We already had a commit that was pending, which will
686
// render 2.
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+192
-51
@@ -61,9 +61,16 @@ describe('ReactIncrementalUpdates', () => {
61
62
ReactNoop.render(<Foo />);
63
expect(Scheduler).toFlushAndYieldThrough(['commit']);
64
- expect(state).toEqual({a: 'a'});
65
- expect(Scheduler).toFlushWithoutYielding();
66
- expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
64
+
65
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
66
+ // TODO: should deferredUpdates flush sync with the default update?
67
+ expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
68
+ expect(Scheduler).toFlushWithoutYielding();
69
+ } else {
70
+ expect(state).toEqual({a: 'a'});
71
+ expect(Scheduler).toFlushWithoutYielding();
72
+ expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
73
+ }
74
});
75
76
it('applies updates with equal priority in insertion order', () => {
@@ -130,6 +137,7 @@ describe('ReactIncrementalUpdates', () => {
137
expect(instance.state).toEqual({c: 'c', d: 'd'});
138
});
139
140
+ // @gate experimental || !enableSyncDefaultUpdates
141
it('can abort an update, schedule additional updates, and resume', () => {
142
let instance;
143
class Foo extends React.Component {
@@ -159,33 +167,75 @@ describe('ReactIncrementalUpdates', () => {
167
}
168
169
// Schedule some async updates
162
- instance.setState(createUpdate('a'));
163
- instance.setState(createUpdate('b'));
164
- instance.setState(createUpdate('c'));
170
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
171
+ React.unstable_startTransition(() => {
172
+ instance.setState(createUpdate('a'));
173
+ instance.setState(createUpdate('b'));
174
+ instance.setState(createUpdate('c'));
175
+ });
176
+ } else {
177
+ instance.setState(createUpdate('a'));
178
+ instance.setState(createUpdate('b'));
179
+ instance.setState(createUpdate('c'));
180
+ }
181
182
// Begin the updates but don't flush them yet
183
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
184
expect(ReactNoop.getChildren()).toEqual([span('')]);
185
186
// Schedule some more updates at different priorities
171
- instance.setState(createUpdate('d'));
172
- ReactNoop.flushSync(() => {
173
- instance.setState(createUpdate('e'));
174
- instance.setState(createUpdate('f'));
175
- });
176
- instance.setState(createUpdate('g'));
187
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
188
+ instance.setState(createUpdate('d'));
189
+ ReactNoop.flushSync(() => {
190
+ instance.setState(createUpdate('e'));
191
+ instance.setState(createUpdate('f'));
192
+ });
193
+ React.unstable_startTransition(() => {
194
+ instance.setState(createUpdate('g'));
195
+ });
196
178
- // The sync updates should have flushed, but not the async ones
179
- expect(Scheduler).toHaveYielded(['e', 'f']);
180
- expect(ReactNoop.getChildren()).toEqual([span('ef')]);
197
+ // The sync updates should have flushed, but not the async ones
198
+ expect(Scheduler).toHaveYielded(['e', 'f']);
199
+ expect(ReactNoop.getChildren()).toEqual([span('ef')]);
200
182
- // Now flush the remaining work. Even though e and f were already processed,
183
- // they should be processed again, to ensure that the terminal state
184
- // is deterministic.
185
- expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
186
- expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
201
+ // Now flush the remaining work. Even though e and f were already processed,
202
+ // they should be processed again, to ensure that the terminal state
203
+ // is deterministic.
204
+ // TODO: should d, e, f be flushed again first?
205
+ expect(Scheduler).toFlushAndYield([
206
+ 'd',
207
+ 'e',
208
+ 'f',
209
+ 'a',
210
+ 'b',
211
+ 'c',
212
+ 'd',
213
+ 'e',
214
+ 'f',
215
+ 'g',
216
+ ]);
217
+ expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
218
+ } else {
219
+ instance.setState(createUpdate('d'));
220
+ ReactNoop.flushSync(() => {
221
+ instance.setState(createUpdate('e'));
222
+ instance.setState(createUpdate('f'));
223
+ });
224
+ instance.setState(createUpdate('g'));
225
+
226
+ // The sync updates should have flushed, but not the async ones
227
+ expect(Scheduler).toHaveYielded(['e', 'f']);
228
+ expect(ReactNoop.getChildren()).toEqual([span('ef')]);
229
+
230
+ // Now flush the remaining work. Even though e and f were already processed,
231
+ // they should be processed again, to ensure that the terminal state
232
+ // is deterministic.
233
+ expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
234
+ expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
235
+ }
236
});
237
238
+ // @gate experimental || !enableSyncDefaultUpdates
239
it('can abort an update, schedule a replaceState, and resume', () => {
240
let instance;
241
class Foo extends React.Component {
@@ -215,34 +265,79 @@ describe('ReactIncrementalUpdates', () => {
265
}
266
267
// Schedule some async updates
218
- instance.setState(createUpdate('a'));
219
- instance.setState(createUpdate('b'));
220
- instance.setState(createUpdate('c'));
268
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
269
+ React.unstable_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'));
278
+ }
279
280
// Begin the updates but don't flush them yet
281
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
282
expect(ReactNoop.getChildren()).toEqual([span('')]);
283
226
- // Schedule some more updates at different priorities{
227
- instance.setState(createUpdate('d'));
228
- ReactNoop.flushSync(() => {
229
- instance.setState(createUpdate('e'));
230
- // No longer a public API, but we can test that it works internally by
231
- // reaching into the updater.
232
- instance.updater.enqueueReplaceState(instance, createUpdate('f'));
233
- });
234
- instance.setState(createUpdate('g'));
235
-
236
- // The sync updates should have flushed, but not the async ones. Update d
237
- // was dropped and replaced by e.
238
- expect(Scheduler).toHaveYielded(['e', 'f']);
239
- expect(ReactNoop.getChildren()).toEqual([span('f')]);
240
-
241
- // Now flush the remaining work. Even though e and f were already processed,
242
- // they should be processed again, to ensure that the terminal state
243
- // is deterministic.
244
- expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
245
- expect(ReactNoop.getChildren()).toEqual([span('fg')]);
284
+ // Schedule some more updates at different priorities
285
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
286
+ instance.setState(createUpdate('d'));
287
+
288
+ ReactNoop.flushSync(() => {
289
+ instance.setState(createUpdate('e'));
290
+ // No longer a public API, but we can test that it works internally by
291
+ // reaching into the updater.
292
+ instance.updater.enqueueReplaceState(instance, createUpdate('f'));
293
+ });
294
+ React.unstable_startTransition(() => {
295
+ instance.setState(createUpdate('g'));
296
+ });
297
+
298
+ // The sync updates should have flushed, but not the async ones.
299
+ // TODO: should 'd' have flushed?
300
+ // TODO: should 'f' have flushed? I don't know what enqueueReplaceState is.
301
+ expect(Scheduler).toHaveYielded(['e', 'f']);
302
+ expect(ReactNoop.getChildren()).toEqual([span('f')]);
303
+
304
+ // Now flush the remaining work. Even though e and f were already processed,
305
+ // they should be processed again, to ensure that the terminal state
306
+ // is deterministic.
307
+ expect(Scheduler).toFlushAndYield([
308
+ 'd',
309
+ 'e',
310
+ 'f',
311
+ 'a',
312
+ 'b',
313
+ 'c',
314
+ 'd',
315
+ 'e',
316
+ 'f',
317
+ 'g',
318
+ ]);
319
+ expect(ReactNoop.getChildren()).toEqual([span('fg')]);
320
+ } else {
321
+ instance.setState(createUpdate('d'));
322
+ ReactNoop.flushSync(() => {
323
+ instance.setState(createUpdate('e'));
324
+ // No longer a public API, but we can test that it works internally by
325
+ // reaching into the updater.
326
+ instance.updater.enqueueReplaceState(instance, createUpdate('f'));
327
+ });
328
+ instance.setState(createUpdate('g'));
329
+
330
+ // The sync updates should have flushed, but not the async ones. Update d
331
+ // was dropped and replaced by e.
332
+ expect(Scheduler).toHaveYielded(['e', 'f']);
333
+ expect(ReactNoop.getChildren()).toEqual([span('f')]);
334
+
335
+ // Now flush the remaining work. Even though e and f were already processed,
336
+ // they should be processed again, to ensure that the terminal state
337
+ // is deterministic.
338
+ expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
339
+ expect(ReactNoop.getChildren()).toEqual([span('fg')]);
340
+ }
341
});
342
343
it('passes accumulation of previous updates to replaceState updater function', () => {
@@ -459,6 +554,7 @@ describe('ReactIncrementalUpdates', () => {
554
expect(ReactNoop.getChildren()).toEqual([span('derived state')]);
555
});
556
557
+ // @gate experimental || !enableSyncDefaultUpdates
558
it('regression: does not expire soon due to layout effects in the last batch', () => {
559
const {useState, useLayoutEffect} = React;
560
@@ -475,7 +571,13 @@ describe('ReactIncrementalUpdates', () => {
571
}
572
573
ReactNoop.act(() => {
478
- ReactNoop.render(<App />);
574
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
575
+ React.unstable_startTransition(() => {
576
+ ReactNoop.render(<App />);
577
+ });
578
+ } else {
579
+ ReactNoop.render(<App />);
580
+ }
581
flushNextRenderIfExpired();
582
expect(Scheduler).toHaveYielded([]);
583
expect(Scheduler).toFlushAndYield([
@@ -485,13 +587,19 @@ describe('ReactIncrementalUpdates', () => {
587
]);
588
589
Scheduler.unstable_advanceTime(10000);
488
-
489
- setCount(2);
590
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
591
+ React.unstable_startTransition(() => {
592
+ setCount(2);
593
+ });
594
+ } else {
595
+ setCount(2);
596
+ }
597
flushNextRenderIfExpired();
598
expect(Scheduler).toHaveYielded([]);
599
});
600
});
601
602
+ // @gate experimental || !enableSyncDefaultUpdates
603
it('regression: does not expire soon due to previous flushSync', () => {
604
function Text({text}) {
605
Scheduler.unstable_yieldValue(text);
@@ -505,29 +613,49 @@ describe('ReactIncrementalUpdates', () => {
613
614
Scheduler.unstable_advanceTime(10000);
615
508
- ReactNoop.render(<Text text="B" />);
616
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
617
+ React.unstable_startTransition(() => {
618
+ ReactNoop.render(<Text text="B" />);
619
+ });
620
+ } else {
621
+ ReactNoop.render(<Text text="B" />);
622
+ }
623
flushNextRenderIfExpired();
624
expect(Scheduler).toHaveYielded([]);
625
});
626
627
+ // @gate experimental || !enableSyncDefaultUpdates
628
it('regression: does not expire soon due to previous expired work', () => {
629
function Text({text}) {
630
Scheduler.unstable_yieldValue(text);
631
return text;
632
}
633
519
- ReactNoop.render(<Text text="A" />);
634
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
635
+ React.unstable_startTransition(() => {
636
+ ReactNoop.render(<Text text="A" />);
637
+ });
638
+ } else {
639
+ ReactNoop.render(<Text text="A" />);
640
+ }
641
Scheduler.unstable_advanceTime(10000);
642
flushNextRenderIfExpired();
643
expect(Scheduler).toHaveYielded(['A']);
644
645
Scheduler.unstable_advanceTime(10000);
646
526
- ReactNoop.render(<Text text="B" />);
647
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
648
+ React.unstable_startTransition(() => {
649
+ ReactNoop.render(<Text text="B" />);
650
+ });
651
+ } else {
652
+ ReactNoop.render(<Text text="B" />);
653
+ }
654
flushNextRenderIfExpired();
655
expect(Scheduler).toHaveYielded([]);
656
});
657
658
+ // @gate experimental || !enableSyncDefaultUpdates
659
it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
660
const {useState, useLayoutEffect} = React;
661
@@ -560,7 +688,13 @@ describe('ReactIncrementalUpdates', () => {
688
expect(root).toMatchRenderedOutput('');
689
690
await ReactNoop.act(async () => {
563
- pushToLog('A');
691
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
692
+ React.unstable_startTransition(() => {
693
+ pushToLog('A');
694
+ });
695
+ } else {
696
+ pushToLog('A');
697
+ }
698
699
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
700
pushToLog('B'),
@@ -584,6 +718,7 @@ describe('ReactIncrementalUpdates', () => {
718
expect(root).toMatchRenderedOutput('ABCD');
719
});
720
721
+ // @gate experimental || !enableSyncDefaultUpdates
722
it('when rebasing, does not exclude updates that were already committed, regardless of priority (classes)', async () => {
723
let pushToLog;
724
class App extends React.Component {
@@ -615,7 +750,13 @@ describe('ReactIncrementalUpdates', () => {
750
expect(root).toMatchRenderedOutput('');
751
752
await ReactNoop.act(async () => {
618
- pushToLog('A');
753
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
754
+ React.unstable_startTransition(() => {
755
+ pushToLog('A');
756
+ });
757
+ } else {
758
+ pushToLog('A');
759
+ }
760
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
761
pushToLog('B'),
762
);
packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js
+16
-2
@@ -22,6 +22,7 @@ describe('ReactInterleavedUpdates', () => {
22
return text;
23
}
24
25
+ // @gate experimental || !enableSyncDefaultUpdates
26
test('update during an interleaved event is not processed during the current render', async () => {
27
const updaters = [];
28
@@ -55,13 +56,25 @@ describe('ReactInterleavedUpdates', () => {
56
expect(root).toMatchRenderedOutput('000');
57
58
await ReactNoop.act(async () => {
58
- updateChildren(1);
59
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
60
+ React.unstable_startTransition(() => {
61
+ updateChildren(1);
62
+ });
63
+ } else {
64
+ updateChildren(1);
65
+ }
66
// Partially render the children. Only the first one.
67
expect(Scheduler).toFlushAndYieldThrough([1]);
68
69
// In an interleaved event, schedule an update on each of the children.
70
// Including the two that haven't rendered yet.
64
- updateChildren(2);
71
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
72
+ React.unstable_startTransition(() => {
73
+ updateChildren(2);
74
+ });
75
+ } else {
76
+ updateChildren(2);
77
+ }
78
79
// We should continue rendering without including the interleaved updates.
80
expect(Scheduler).toFlushUntilNextPaint([1, 1]);
@@ -73,6 +86,7 @@ describe('ReactInterleavedUpdates', () => {
86
});
87
88
// @gate experimental
89
+ // @gate !enableSyncDefaultUpdates
90
test('low priority update during an interleaved event is not processed during the current render', async () => {
91
// Same as previous test, but the interleaved update is lower priority than
92
// the in-progress render.
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+8
-1
@@ -1475,6 +1475,7 @@ describe('ReactLazy', () => {
1475
});
1476
1477
// @gate enableLazyElements
1478
+ // @gate experimental || !enableSyncDefaultUpdates
1479
it('mount and reorder lazy elements', async () => {
1480
class Child extends React.Component {
1481
componentDidMount() {
@@ -1534,7 +1535,13 @@ describe('ReactLazy', () => {
1535
expect(root).toMatchRenderedOutput('AB');
1536
1537
// Swap the position of A and B
1537
- root.update(<Parent swap={true} />);
1538
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1539
+ React.unstable_startTransition(() => {
1540
+ root.update(<Parent swap={true} />);
1541
+ });
1542
+ } else {
1543
+ root.update(<Parent swap={true} />);
1544
+ }
1545
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1546
await lazyChildB2;
1547
// We need to flush to trigger the second one to load.
packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+8
-1
@@ -828,6 +828,7 @@ describe('ReactNewContext', () => {
828
);
829
});
830
831
+ // @gate experimental || !enableSyncDefaultUpdates
832
it('warns if multiple renderers concurrently render the same context', () => {
833
spyOnDev(console, 'error');
834
const Context = React.createContext(0);
@@ -846,7 +847,13 @@ describe('ReactNewContext', () => {
847
);
848
}
849
849
- ReactNoop.render(<App value={1} />);
850
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
851
+ React.unstable_startTransition(() => {
852
+ ReactNoop.render(<App value={1} />);
853
+ });
854
+ } else {
855
+ ReactNoop.render(<App value={1} />);
856
+ }
857
// Render past the Provider, but don't commit yet
858
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
859
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+8
-1
@@ -88,6 +88,7 @@ describe('ReactSchedulerIntegration', () => {
88
]);
89
});
90
91
+ // @gate experimental || !enableSyncDefaultUpdates
92
it('requests a paint after committing', () => {
93
const scheduleCallback = Scheduler.unstable_scheduleCallback;
94
@@ -100,7 +101,13 @@ describe('ReactSchedulerIntegration', () => {
101
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
102
103
// Schedule a React render. React will request a paint after committing it.
103
- root.render('Update');
104
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
105
+ React.unstable_startTransition(() => {
106
+ root.render('Update');
107
+ });
108
+ } else {
109
+ root.render('Update');
110
+ }
111
112
// Advance time just to be sure the next tasks have lower priority
113
Scheduler.unstable_advanceTime(2000);
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+33
-10
@@ -99,6 +99,7 @@ describe('ReactSuspense', () => {
99
}
100
}
101
102
+ // @gate experimental || !enableSyncDefaultUpdates
103
it('suspends rendering and continues later', () => {
104
function Bar(props) {
105
Scheduler.unstable_yieldValue('Bar');
@@ -129,7 +130,13 @@ describe('ReactSuspense', () => {
130
131
// Navigate the shell to now render the child content.
132
// This should suspend.
132
- root.update(<Foo renderBar={true} />);
133
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
134
+ React.unstable_startTransition(() => {
135
+ root.update(<Foo renderBar={true} />);
136
+ });
137
+ } else {
138
+ root.update(<Foo renderBar={true} />);
139
+ }
140
141
expect(Scheduler).toFlushAndYield([
142
'Foo',
@@ -197,6 +204,7 @@ describe('ReactSuspense', () => {
204
expect(root).toMatchRenderedOutput('AB');
205
});
206
207
+ // @gate experimental || !enableSyncDefaultUpdates
208
it('interrupts current render if promise resolves before current render phase', () => {
209
let didResolve = false;
210
const listeners = [];
@@ -238,15 +246,29 @@ describe('ReactSuspense', () => {
246
expect(root).toMatchRenderedOutput('Initial');
247
248
// The update will suspend.
241
- root.update(
242
- <>
243
- <Suspense fallback={<Text text="Loading..." />}>
244
- <Async />
245
- </Suspense>
246
- <Text text="After Suspense" />
247
- <Text text="Sibling" />
248
- </>,
249
- );
249
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
250
+ React.unstable_startTransition(() => {
251
+ root.update(
252
+ <>
253
+ <Suspense fallback={<Text text="Loading..." />}>
254
+ <Async />
255
+ </Suspense>
256
+ <Text text="After Suspense" />
257
+ <Text text="Sibling" />
258
+ </>,
259
+ );
260
+ });
261
+ } else {
262
+ root.update(
263
+ <>
264
+ <Suspense fallback={<Text text="Loading..." />}>
265
+ <Async />
266
+ </Suspense>
267
+ <Text text="After Suspense" />
268
+ <Text text="Sibling" />
269
+ </>,
270
+ );
271
+ }
272
273
// Yield past the Suspense boundary but don't complete the last sibling.
274
expect(Scheduler).toFlushAndYieldThrough([
@@ -271,6 +293,7 @@ describe('ReactSuspense', () => {
293
});
294
295
// @gate experimental
296
+ // @gate !enableSyncDefaultUpdates
297
it(
298
'interrupts current render when something suspends with a ' +
299
"delay and we've already skipped over a lower priority update in " +
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+35
-5
@@ -1274,7 +1274,13 @@ describe('ReactSuspenseList', () => {
1274
}
1275
1276
// This render is only CPU bound. Nothing suspends.
1277
- ReactNoop.render(<Foo />);
1277
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1278
+ React.unstable_startTransition(() => {
1279
+ ReactNoop.render(<Foo />);
1280
+ });
1281
+ } else {
1282
+ ReactNoop.render(<Foo />);
1283
+ }
1284
1285
expect(Scheduler).toFlushAndYieldThrough(['A']);
1286
@@ -1452,7 +1458,13 @@ describe('ReactSuspenseList', () => {
1458
}
1459
1460
// This render is only CPU bound. Nothing suspends.
1455
- ReactNoop.render(<Foo />);
1461
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1462
+ React.unstable_startTransition(() => {
1463
+ ReactNoop.render(<Foo />);
1464
+ });
1465
+ } else {
1466
+ ReactNoop.render(<Foo />);
1467
+ }
1468
1469
expect(Scheduler).toFlushAndYieldThrough(['A']);
1470
@@ -2449,7 +2461,13 @@ describe('ReactSuspenseList', () => {
2461
2462
await ReactNoop.act(async () => {
2463
// Add a few items at the end.
2452
- updateLowPri(true);
2464
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2465
+ React.unstable_startTransition(() => {
2466
+ updateLowPri(true);
2467
+ });
2468
+ } else {
2469
+ updateLowPri(true);
2470
+ }
2471
2472
// Flush partially through.
2473
expect(Scheduler).toFlushAndYieldThrough(['B', 'C']);
@@ -2586,7 +2604,13 @@ describe('ReactSuspenseList', () => {
2604
);
2605
}
2606
2589
- ReactNoop.render(<App />);
2607
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2608
+ React.unstable_startTransition(() => {
2609
+ ReactNoop.render(<App />);
2610
+ });
2611
+ } else {
2612
+ ReactNoop.render(<App />);
2613
+ }
2614
2615
expect(Scheduler).toFlushAndYieldThrough([
2616
'App',
@@ -2653,7 +2677,13 @@ describe('ReactSuspenseList', () => {
2677
);
2678
}
2679
2656
- ReactNoop.render(<App />);
2680
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2681
+ React.unstable_startTransition(() => {
2682
+ ReactNoop.render(<App />);
2683
+ });
2684
+ } else {
2685
+ ReactNoop.render(<App />);
2686
+ }
2687
2688
expect(Scheduler).toFlushAndYieldThrough([
2689
'App',
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+224
-37
@@ -211,7 +211,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
211
);
212
}
213
214
- ReactNoop.render(<Foo />);
214
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
215
+ React.unstable_startTransition(() => {
216
+ ReactNoop.render(<Foo />);
217
+ });
218
+ } else {
219
+ ReactNoop.render(<Foo />);
220
+ }
221
expect(Scheduler).toFlushAndYieldThrough([
222
'Foo',
223
'Bar',
@@ -277,7 +283,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
283
expect(Scheduler).toFlushAndYield(['Foo']);
284
285
// The update will suspend.
280
- ReactNoop.render(<Foo renderBar={true} />);
286
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
287
+ React.unstable_startTransition(() => {
288
+ ReactNoop.render(<Foo renderBar={true} />);
289
+ });
290
+ } else {
291
+ ReactNoop.render(<Foo renderBar={true} />);
292
+ }
293
expect(Scheduler).toFlushAndYield([
294
'Foo',
295
'Bar',
@@ -341,14 +353,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
353
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
354
expect(Scheduler).toFlushAndYield([]);
355
// B suspends. Continue rendering the remaining siblings.
344
- ReactNoop.render(
345
- <Suspense fallback={<Text text="Loading..." />}>
346
- <Text text="A" />
347
- <AsyncText text="B" />
348
- <Text text="C" />
349
- <Text text="D" />
350
- </Suspense>,
351
- );
356
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
357
+ React.unstable_startTransition(() => {
358
+ ReactNoop.render(
359
+ <Suspense fallback={<Text text="Loading..." />}>
360
+ <Text text="A" />
361
+ <AsyncText text="B" />
362
+ <Text text="C" />
363
+ <Text text="D" />
364
+ </Suspense>,
365
+ );
366
+ });
367
+ } else {
368
+ ReactNoop.render(
369
+ <Suspense fallback={<Text text="Loading..." />}>
370
+ <Text text="A" />
371
+ <AsyncText text="B" />
372
+ <Text text="C" />
373
+ <Text text="D" />
374
+ </Suspense>,
375
+ );
376
+ }
377
// B suspends. Continue rendering the remaining siblings.
378
expect(Scheduler).toFlushAndYield([
379
'A',
@@ -376,6 +401,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
401
// TODO: Delete this feature flag.
402
// @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
403
// @gate enableCache
404
+ // @gate experimental || !enableSyncDefaultUpdates
405
it('retries on error', async () => {
406
class ErrorBoundary extends React.Component {
407
state = {error: null};
@@ -410,7 +436,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
436
expect(Scheduler).toFlushAndYield([]);
437
expect(ReactNoop.getChildren()).toEqual([]);
438
413
- ReactNoop.render(<App renderContent={true} />);
439
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
440
+ React.unstable_startTransition(() => {
441
+ ReactNoop.render(<App renderContent={true} />);
442
+ });
443
+ } else {
444
+ ReactNoop.render(<App renderContent={true} />);
445
+ }
446
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
447
expect(ReactNoop.getChildren()).toEqual([]);
448
@@ -535,6 +567,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
567
});
568
569
// @gate enableCache
570
+ // @gate experimental || !enableSyncDefaultUpdates
571
it('keeps working on lower priority work after being pinged', async () => {
572
// Advance the virtual time so that we're close to the edge of a bucket.
573
ReactNoop.expire(149);
@@ -552,14 +585,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
585
expect(Scheduler).toFlushAndYield([]);
586
expect(ReactNoop.getChildren()).toEqual([]);
587
555
- ReactNoop.render(<App showA={true} showB={false} />);
588
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
589
+ React.unstable_startTransition(() => {
590
+ ReactNoop.render(<App showA={true} showB={false} />);
591
+ });
592
+ } else {
593
+ ReactNoop.render(<App showA={true} showB={false} />);
594
+ }
595
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
596
expect(ReactNoop.getChildren()).toEqual([]);
597
598
// Advance React's virtual time by enough to fall into a new async bucket,
599
// but not enough to expire the suspense timeout.
600
ReactNoop.expire(120);
562
- ReactNoop.render(<App showA={true} showB={true} />);
601
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
602
+ React.unstable_startTransition(() => {
603
+ ReactNoop.render(<App showA={true} showB={true} />);
604
+ });
605
+ } else {
606
+ ReactNoop.render(<App showA={true} showB={true} />);
607
+ }
608
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
609
expect(ReactNoop.getChildren()).toEqual([]);
610
@@ -602,6 +647,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
647
// times model. Might not make sense in the new model.
648
// TODO: This test doesn't over what it was originally designed to test.
649
// Either rewrite or delete.
650
+ // @gate experimental || !enableSyncDefaultUpdates
651
it('tries each subsequent level after suspending', async () => {
652
const root = ReactNoop.createRoot();
653
@@ -636,17 +682,35 @@ describe('ReactSuspenseWithNoopRenderer', () => {
682
683
// Schedule an update at several distinct expiration times
684
await ReactNoop.act(async () => {
639
- root.render(<App step={1} shouldSuspend={true} />);
685
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
686
+ React.unstable_startTransition(() => {
687
+ root.render(<App step={1} shouldSuspend={true} />);
688
+ });
689
+ } else {
690
+ root.render(<App step={1} shouldSuspend={true} />);
691
+ }
692
Scheduler.unstable_advanceTime(1000);
693
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
694
interrupt();
695
644
- root.render(<App step={2} shouldSuspend={true} />);
696
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
697
+ React.unstable_startTransition(() => {
698
+ root.render(<App step={2} shouldSuspend={true} />);
699
+ });
700
+ } else {
701
+ root.render(<App step={2} shouldSuspend={true} />);
702
+ }
703
Scheduler.unstable_advanceTime(1000);
704
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
705
interrupt();
706
649
- root.render(<App step={3} shouldSuspend={true} />);
707
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
708
+ React.unstable_startTransition(() => {
709
+ root.render(<App step={3} shouldSuspend={true} />);
710
+ });
711
+ } else {
712
+ root.render(<App step={3} shouldSuspend={true} />);
713
+ }
714
Scheduler.unstable_advanceTime(1000);
715
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
716
interrupt();
@@ -864,22 +928,80 @@ describe('ReactSuspenseWithNoopRenderer', () => {
928
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
929
});
930
867
- // @gate enableCache
868
- it('resolves successfully even if fallback render is pending', async () => {
931
+ // @gate experimental
932
+ it('does not expire for transitions', async () => {
933
ReactNoop.render(
870
- <>
934
+ <Fragment>
935
<Suspense fallback={<Text text="Loading..." />} />
872
- </>,
936
+ </Fragment>,
937
);
938
expect(Scheduler).toFlushAndYield([]);
939
+
940
+ React.unstable_startTransition(() => {
941
+ ReactNoop.render(
942
+ <Fragment>
943
+ <Suspense fallback={<Text text="Loading..." />}>
944
+ <AsyncText text="Async" />
945
+ </Suspense>
946
+ <Text text="Sync" />
947
+ </Fragment>,
948
+ );
949
+ });
950
+
951
+ expect(Scheduler).toFlushAndYield([
952
+ // The async child suspends
953
+ 'Suspend! [Async]',
954
+ 'Loading...',
955
+ // Continue on the sibling
956
+ 'Sync',
957
+ ]);
958
+ // The update hasn't expired yet, so we commit nothing.
959
+ expect(ReactNoop.getChildren()).toEqual([]);
960
+
961
+ // Advance both React's virtual time and Jest's timers,
962
+ // but not by enough to flush the promise or reach the true expiration time.
963
+ ReactNoop.expire(2000);
964
+ await advanceTimers(2000);
965
expect(ReactNoop.getChildren()).toEqual([]);
966
+
967
+ // Even flushing won't yield a fallback in a transition.
968
+ expect(Scheduler).toFlushWithoutYielding();
969
+ expect(ReactNoop.getChildren()).toEqual([]);
970
+
971
+ // Once the promise resolves, we render the suspended view
972
+ await resolveText('Async');
973
+ expect(Scheduler).toFlushAndYield(['Async', 'Sync']);
974
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
975
+ });
976
+
977
+ // @gate enableCache
978
+ it('resolves successfully even if fallback render is pending', async () => {
979
ReactNoop.render(
980
<>
878
- <Suspense fallback={<Text text="Loading..." />}>
879
- <AsyncText text="Async" />
880
- </Suspense>
981
+ <Suspense fallback={<Text text="Loading..." />} />
982
</>,
983
);
984
+ expect(Scheduler).toFlushAndYield([]);
985
+ expect(ReactNoop.getChildren()).toEqual([]);
986
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
987
+ React.unstable_startTransition(() => {
988
+ ReactNoop.render(
989
+ <>
990
+ <Suspense fallback={<Text text="Loading..." />}>
991
+ <AsyncText text="Async" />
992
+ </Suspense>
993
+ </>,
994
+ );
995
+ });
996
+ } else {
997
+ ReactNoop.render(
998
+ <>
999
+ <Suspense fallback={<Text text="Loading..." />}>
1000
+ <AsyncText text="Async" />
1001
+ </Suspense>
1002
+ </>,
1003
+ );
1004
+ }
1005
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
1006
1007
await resolveText('Async');
@@ -928,11 +1050,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1050
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
1051
expect(Scheduler).toFlushAndYield([]);
1052
931
- ReactNoop.render(
932
- <Suspense fallback={<Text text="Loading..." />}>
933
- <AsyncText text="Async" />
934
- </Suspense>,
935
- );
1053
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1054
+ React.unstable_startTransition(() => {
1055
+ ReactNoop.render(
1056
+ <Suspense fallback={<Text text="Loading..." />}>
1057
+ <AsyncText text="Async" />
1058
+ </Suspense>,
1059
+ );
1060
+ });
1061
+ } else {
1062
+ ReactNoop.render(
1063
+ <Suspense fallback={<Text text="Loading..." />}>
1064
+ <AsyncText text="Async" />
1065
+ </Suspense>,
1066
+ );
1067
+ }
1068
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
1069
expect(ReactNoop.getChildren()).toEqual([]);
1070
@@ -1725,6 +1857,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1857
});
1858
1859
// @gate enableCache
1860
+ // @gate experimental || !enableSyncDefaultUpdates
1861
it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1862
function Foo({renderContent}) {
1863
Scheduler.unstable_yieldValue('Foo');
@@ -1738,7 +1871,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1871
ReactNoop.render(<Foo />);
1872
expect(Scheduler).toFlushAndYield(['Foo']);
1873
1741
- ReactNoop.render(<Foo renderContent={true} />);
1874
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1875
+ React.unstable_startTransition(() => {
1876
+ ReactNoop.render(<Foo renderContent={true} />);
1877
+ });
1878
+ } else {
1879
+ ReactNoop.render(<Foo renderContent={true} />);
1880
+ }
1881
Scheduler.unstable_advanceTime(100);
1882
await advanceTimers(100);
1883
// Start rendering
@@ -1767,12 +1906,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1906
await advanceTimers(500);
1907
// No need to rerender.
1908
expect(Scheduler).toFlushWithoutYielding();
1770
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1909
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1910
+ // Since this is a transition, we never fallback.
1911
+ expect(ReactNoop.getChildren()).toEqual([]);
1912
+ } else {
1913
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1914
+ }
1915
1916
// Flush the promise completely
1917
await resolveText('A');
1918
// Renders successfully
1775
- expect(Scheduler).toFlushAndYield(['A']);
1919
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1920
+ // TODO: Why does this render Foo
1921
+ expect(Scheduler).toFlushAndYield(['Foo', 'A']);
1922
+ } else {
1923
+ expect(Scheduler).toFlushAndYield(['A']);
1924
+ }
1925
expect(ReactNoop.getChildren()).toEqual([span('A')]);
1926
});
1927
@@ -1913,7 +2062,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2062
ReactNoop.render(<Foo />);
2063
expect(Scheduler).toFlushAndYield(['Foo']);
2064
1916
- ReactNoop.render(<Foo renderContent={true} />);
2065
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2066
+ React.unstable_startTransition(() => {
2067
+ ReactNoop.render(<Foo renderContent={true} />);
2068
+ });
2069
+ } else {
2070
+ ReactNoop.render(<Foo renderContent={true} />);
2071
+ }
2072
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
2073
2074
// Advance some time.
@@ -1938,7 +2093,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2093
// updates as way earlier in the past. This test ensures that we don't
2094
// use this assumption to add a very long JND.
2095
expect(Scheduler).toFlushWithoutYielding();
1941
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2096
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2097
+ // Transitions never fallback.
2098
+ expect(ReactNoop.getChildren()).toEqual([]);
2099
+ } else {
2100
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2101
+ }
2102
});
2103
2104
// TODO: flip to "warns" when this is implemented again.
@@ -2223,7 +2383,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2383
expect(Scheduler).toFlushAndYield(['Foo', 'A']);
2384
expect(ReactNoop.getChildren()).toEqual([span('A')]);
2385
2226
- ReactNoop.render(<Foo showB={true} />);
2386
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2387
+ React.unstable_startTransition(() => {
2388
+ ReactNoop.render(<Foo showB={true} />);
2389
+ });
2390
+ } else {
2391
+ ReactNoop.render(<Foo showB={true} />);
2392
+ }
2393
2394
expect(Scheduler).toFlushAndYield([
2395
'Foo',
@@ -2238,7 +2404,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2404
Scheduler.unstable_advanceTime(600);
2405
await advanceTimers(600);
2406
2241
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
2407
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2408
+ // Transitions never fall back.
2409
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2410
+ } else {
2411
+ expect(ReactNoop.getChildren()).toEqual([
2412
+ span('A'),
2413
+ span('Loading B...'),
2414
+ ]);
2415
+ }
2416
});
2417
2418
// @gate enableCache
@@ -2757,14 +2931,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2931
await ReactNoop.act(async () => {
2932
// Update. Since showing a fallback would hide content that's already
2933
// visible, it should suspend for a JND without committing.
2760
- root.render(<App text="First update" />);
2934
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2935
+ React.unstable_startTransition(() => {
2936
+ root.render(<App text="First update" />);
2937
+ });
2938
+ } else {
2939
+ root.render(<App text="First update" />);
2940
+ }
2941
expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
2942
2943
// Should not display a fallback
2944
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2945
2946
// Update again. This should also suspend for a JND.
2767
- root.render(<App text="Second update" />);
2947
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2948
+ React.unstable_startTransition(() => {
2949
+ root.render(<App text="Second update" />);
2950
+ });
2951
+ } else {
2952
+ root.render(<App text="Second update" />);
2953
+ }
2954
expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
2955
2956
// Should not display a fallback
@@ -3540,6 +3726,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3726
3727
// @gate experimental
3728
// @gate enableCache
3729
+ // @gate !enableSyncDefaultUpdates
3730
it('regression: ping at high priority causes update to be dropped', async () => {
3731
const {useState, unstable_useTransition: useTransition} = React;
3732
packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js
+5
-1
@@ -452,7 +452,11 @@ describe('updaters', () => {
452
};
453
const LowPriorityUpdater = () => {
454
const [count, setCount] = React.useState(0);
455
- triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1);
455
+ triggerLowPriorityUpdate = () => {
456
+ React.unstable_startTransition(() => {
457
+ setCount(prevCount => prevCount + 1);
458
+ });
459
+ };
460
Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`);
461
return <Yield value={`LowPriority ${count}`} />;
462
};
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+36
-16
@@ -13,12 +13,19 @@
13
// This test is *.internal so that it can import this shared file.
14
import ReactVersion from 'shared/ReactVersion';
15
16
+// Hard-coding because importing will not work with bundle tests and to
17
+// avoid leaking exports for lanes that are only imported in this test.
18
+const ReactFiberLane = {
19
+ SyncLane: /* */ 0b0000000000000000000000000000001,
20
+ DefaultLane: /* */ 0b0000000000000000000000000010000,
21
+ TransitionLane1: /* */ 0b0000000000000000000000001000000,
22
+};
23
+
24
describe('SchedulingProfiler', () => {
25
let React;
26
let ReactTestRenderer;
27
let ReactNoop;
28
let Scheduler;
21
- let ReactFiberLane;
29
30
let clearedMarks;
31
let featureDetectionMarkName = null;
@@ -82,11 +89,6 @@ describe('SchedulingProfiler', () => {
89
90
const SchedulingProfiler = require('react-reconciler/src/SchedulingProfiler');
91
formatLanes = SchedulingProfiler.formatLanes;
85
-
86
- const ReactFeatureFlags = require('shared/ReactFeatureFlags');
87
- ReactFiberLane = ReactFeatureFlags.enableNewReconciler
88
- ? require('react-reconciler/src/ReactFiberLane.new')
89
- : require('react-reconciler/src/ReactFiberLane.old');
92
});
93
94
afterEach(() => {
@@ -147,6 +149,7 @@ describe('SchedulingProfiler', () => {
149
});
150
151
// @gate enableSchedulingProfiler
152
+ // @gate experimental || !enableSyncDefaultUpdates
153
it('should mark render yields', async () => {
154
function Bar() {
155
Scheduler.unstable_yieldValue('Bar');
@@ -158,16 +161,33 @@ describe('SchedulingProfiler', () => {
161
return <Bar />;
162
}
163
161
- ReactNoop.render(<Foo />);
162
- // Do one step of work.
163
- expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
164
-
165
- expectMarksToEqual([
166
- `--react-init-${ReactVersion}`,
167
- `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
168
- `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
169
- '--render-yield',
170
- ]);
164
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
165
+ React.unstable_startTransition(() => {
166
+ ReactNoop.render(<Foo />);
167
+ });
168
+
169
+ // Do one step of work.
170
+ expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
171
+
172
+ expectMarksToEqual([
173
+ `--react-init-${ReactVersion}`,
174
+ `--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`,
175
+ `--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`,
176
+ '--render-yield',
177
+ ]);
178
+ } else {
179
+ ReactNoop.render(<Foo />);
180
+
181
+ // Do one step of work.
182
+ expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
183
+
184
+ expectMarksToEqual([
185
+ `--react-init-${ReactVersion}`,
186
+ `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
187
+ `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
188
+ '--render-yield',
189
+ ]);
190
+ }
191
});
192
193
// @gate enableSchedulingProfiler
packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
+13
-5
@@ -168,10 +168,18 @@ describe('SchedulingProfiler labels', () => {
168
event.initEvent('mouseover', true, true);
169
dispatchAndSetCurrentEvent(targetRef.current, event);
170
});
171
- expect(clearedMarks).toContain(
172
- `--schedule-state-update-${formatLanes(
173
- ReactFiberLane.InputContinuousLane,
174
- )}-App`,
175
- );
171
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
172
+ expect(clearedMarks).toContain(
173
+ `--schedule-state-update-${formatLanes(
174
+ ReactFiberLane.DefaultLane,
175
+ )}-App`,
176
+ );
177
+ } else {
178
+ expect(clearedMarks).toContain(
179
+ `--schedule-state-update-${formatLanes(
180
+ ReactFiberLane.InputContinuousLane,
181
+ )}-App`,
182
+ );
183
+ }
184
});
185
});
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+278
-120
@@ -213,24 +213,45 @@ describe('useMutableSource', () => {
213
const mutableSource = createMutableSource(source, param => param.version);
214
215
act(() => {
216
- ReactNoop.render(
217
- <>
218
- <Component
219
- label="a"
220
- getSnapshot={defaultGetSnapshot}
221
- mutableSource={mutableSource}
222
- subscribe={defaultSubscribe}
223
- />
224
- <Component
225
- label="b"
226
- getSnapshot={defaultGetSnapshot}
227
- mutableSource={mutableSource}
228
- subscribe={defaultSubscribe}
229
- />
230
- </>,
231
- () => Scheduler.unstable_yieldValue('Sync effect'),
232
- );
233
-
216
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
217
+ React.unstable_startTransition(() => {
218
+ ReactNoop.render(
219
+ <>
220
+ <Component
221
+ label="a"
222
+ getSnapshot={defaultGetSnapshot}
223
+ mutableSource={mutableSource}
224
+ subscribe={defaultSubscribe}
225
+ />
226
+ <Component
227
+ label="b"
228
+ getSnapshot={defaultGetSnapshot}
229
+ mutableSource={mutableSource}
230
+ subscribe={defaultSubscribe}
231
+ />
232
+ </>,
233
+ () => Scheduler.unstable_yieldValue('Sync effect'),
234
+ );
235
+ });
236
+ } else {
237
+ ReactNoop.render(
238
+ <>
239
+ <Component
240
+ label="a"
241
+ getSnapshot={defaultGetSnapshot}
242
+ mutableSource={mutableSource}
243
+ subscribe={defaultSubscribe}
244
+ />
245
+ <Component
246
+ label="b"
247
+ getSnapshot={defaultGetSnapshot}
248
+ mutableSource={mutableSource}
249
+ subscribe={defaultSubscribe}
250
+ />
251
+ </>,
252
+ () => Scheduler.unstable_yieldValue('Sync effect'),
253
+ );
254
+ }
255
// Do enough work to read from one component
256
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
257
@@ -431,7 +452,13 @@ describe('useMutableSource', () => {
452
453
// Changing values should schedule an update with React.
454
// Start working on this update but don't finish it.
434
- source.value = 'two';
455
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
456
+ React.unstable_startTransition(() => {
457
+ source.value = 'two';
458
+ });
459
+ } else {
460
+ source.value = 'two';
461
+ }
462
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
463
464
// Re-renders that occur before the update is processed
@@ -695,29 +722,57 @@ describe('useMutableSource', () => {
722
723
// Because the store has not changed yet, there are no pending updates,
724
// so it is considered safe to read from when we start this render.
698
- ReactNoop.render(
699
- <>
700
- <Component
701
- label="a"
702
- getSnapshot={getSnapshotA}
703
- mutableSource={mutableSource}
704
- subscribe={subscribeA}
705
- />
706
- <Component
707
- label="b"
708
- getSnapshot={getSnapshotB}
709
- mutableSource={mutableSource}
710
- subscribe={subscribeB}
711
- />
712
- <Component
713
- label="c"
714
- getSnapshot={getSnapshotB}
715
- mutableSource={mutableSource}
716
- subscribe={subscribeB}
717
- />
718
- </>,
719
- () => Scheduler.unstable_yieldValue('Sync effect'),
720
- );
725
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
726
+ React.unstable_startTransition(() => {
727
+ ReactNoop.render(
728
+ <>
729
+ <Component
730
+ label="a"
731
+ getSnapshot={getSnapshotA}
732
+ mutableSource={mutableSource}
733
+ subscribe={subscribeA}
734
+ />
735
+ <Component
736
+ label="b"
737
+ getSnapshot={getSnapshotB}
738
+ mutableSource={mutableSource}
739
+ subscribe={subscribeB}
740
+ />
741
+ <Component
742
+ label="c"
743
+ getSnapshot={getSnapshotB}
744
+ mutableSource={mutableSource}
745
+ subscribe={subscribeB}
746
+ />
747
+ </>,
748
+ () => Scheduler.unstable_yieldValue('Sync effect'),
749
+ );
750
+ });
751
+ } else {
752
+ ReactNoop.render(
753
+ <>
754
+ <Component
755
+ label="a"
756
+ getSnapshot={getSnapshotA}
757
+ mutableSource={mutableSource}
758
+ subscribe={subscribeA}
759
+ />
760
+ <Component
761
+ label="b"
762
+ getSnapshot={getSnapshotB}
763
+ mutableSource={mutableSource}
764
+ subscribe={subscribeB}
765
+ />
766
+ <Component
767
+ label="c"
768
+ getSnapshot={getSnapshotB}
769
+ mutableSource={mutableSource}
770
+ subscribe={subscribeB}
771
+ />
772
+ </>,
773
+ () => Scheduler.unstable_yieldValue('Sync effect'),
774
+ );
775
+ }
776
expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
777
778
// Mutating the source should trigger a tear detection on the next read,
@@ -808,22 +863,43 @@ describe('useMutableSource', () => {
863
864
act(() => {
865
// Start a render that uses the mutable source.
811
- ReactNoop.render(
812
- <>
813
- <Component
814
- label="a"
815
- getSnapshot={defaultGetSnapshot}
816
- mutableSource={mutableSource}
817
- subscribe={defaultSubscribe}
818
- />
819
- <Component
820
- label="b"
821
- getSnapshot={defaultGetSnapshot}
822
- mutableSource={mutableSource}
823
- subscribe={defaultSubscribe}
824
- />
825
- </>,
826
- );
866
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
867
+ React.unstable_startTransition(() => {
868
+ ReactNoop.render(
869
+ <>
870
+ <Component
871
+ label="a"
872
+ getSnapshot={defaultGetSnapshot}
873
+ mutableSource={mutableSource}
874
+ subscribe={defaultSubscribe}
875
+ />
876
+ <Component
877
+ label="b"
878
+ getSnapshot={defaultGetSnapshot}
879
+ mutableSource={mutableSource}
880
+ subscribe={defaultSubscribe}
881
+ />
882
+ </>,
883
+ );
884
+ });
885
+ } else {
886
+ ReactNoop.render(
887
+ <>
888
+ <Component
889
+ label="a"
890
+ getSnapshot={defaultGetSnapshot}
891
+ mutableSource={mutableSource}
892
+ subscribe={defaultSubscribe}
893
+ />
894
+ <Component
895
+ label="b"
896
+ getSnapshot={defaultGetSnapshot}
897
+ mutableSource={mutableSource}
898
+ subscribe={defaultSubscribe}
899
+ />
900
+ </>,
901
+ );
902
+ }
903
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
904
905
// Mutate source
@@ -1457,13 +1533,25 @@ describe('useMutableSource', () => {
1533
expect(root).toMatchRenderedOutput('a0');
1534
1535
await act(async () => {
1460
- root.render(
1461
- <>
1462
- <Read getSnapshot={getSnapshotA} />
1463
- <Read getSnapshot={getSnapshotB} />
1464
- <Text text="c" />
1465
- </>,
1466
- );
1536
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1537
+ React.unstable_startTransition(() => {
1538
+ root.render(
1539
+ <>
1540
+ <Read getSnapshot={getSnapshotA} />
1541
+ <Read getSnapshot={getSnapshotB} />
1542
+ <Text text="c" />
1543
+ </>,
1544
+ );
1545
+ });
1546
+ } else {
1547
+ root.render(
1548
+ <>
1549
+ <Read getSnapshot={getSnapshotA} />
1550
+ <Read getSnapshot={getSnapshotB} />
1551
+ <Text text="c" />
1552
+ </>,
1553
+ );
1554
+ }
1555
1556
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
1557
// Mutate in an event. This schedules a subscription update on a, which
@@ -1481,10 +1569,16 @@ describe('useMutableSource', () => {
1569
mutateB('b0');
1570
});
1571
// Finish the current render
1484
- expect(Scheduler).toFlushUntilNextPaint(['c']);
1485
- // a0 will re-render because of the mutation update. But it should show
1486
- // the latest value, not the intermediate one, to avoid tearing with b.
1487
- expect(Scheduler).toFlushUntilNextPaint(['a0']);
1572
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1573
+ // Default sync will flush both without yielding
1574
+ expect(Scheduler).toFlushUntilNextPaint(['c', 'a0']);
1575
+ } else {
1576
+ expect(Scheduler).toFlushUntilNextPaint(['c']);
1577
+ // a0 will re-render because of the mutation update. But it should show
1578
+ // the latest value, not the intermediate one, to avoid tearing with b.
1579
+ expect(Scheduler).toFlushUntilNextPaint(['a0']);
1580
+ }
1581
+
1582
expect(root).toMatchRenderedOutput('a0b0c');
1583
// We should be done.
1584
expect(Scheduler).toFlushAndYield([]);
@@ -1591,7 +1685,13 @@ describe('useMutableSource', () => {
1685
1686
await act(async () => {
1687
// Switch the parent and the child to read using the same config
1594
- root.render(<App parentConfig={configB} childConfig={configB} />);
1688
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1689
+ React.unstable_startTransition(() => {
1690
+ root.render(<App parentConfig={configB} childConfig={configB} />);
1691
+ });
1692
+ } else {
1693
+ root.render(<App parentConfig={configB} childConfig={configB} />);
1694
+ }
1695
// Start rendering the parent, but yield before rendering the child
1696
expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
1697
@@ -1602,25 +1702,41 @@ describe('useMutableSource', () => {
1702
source.valueB = '3';
1703
});
1704
1605
- expect(Scheduler).toFlushAndYieldThrough([
1606
- // The partial render completes
1607
- 'Child: 2',
1608
- 'Commit: 2, 2',
1609
- ]);
1705
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1706
+ // In default sync mode, all of the updates flush sync.
1707
+ expect(Scheduler).toFlushAndYieldThrough([
1708
+ // The partial render completes
1709
+ 'Child: 2',
1710
+ 'Commit: 2, 2',
1711
+ 'Parent: 3',
1712
+ 'Child: 3',
1713
+ ]);
1714
1611
- // Now there are two pending mutations at different priorities. But they
1612
- // both read the same version of the mutable source, so we must render
1613
- // them simultaneously.
1614
- //
1615
- expect(Scheduler).toFlushAndYieldThrough([
1616
- 'Parent: 3',
1617
- // Demonstrates that we can yield here
1618
- ]);
1619
- expect(Scheduler).toFlushAndYield([
1620
- // Now finish the rest of the update
1621
- 'Child: 3',
1622
- 'Commit: 3, 3',
1623
- ]);
1715
+ expect(Scheduler).toFlushAndYield([
1716
+ // Now finish the rest of the update
1717
+ 'Commit: 3, 3',
1718
+ ]);
1719
+ } else {
1720
+ expect(Scheduler).toFlushAndYieldThrough([
1721
+ // The partial render completes
1722
+ 'Child: 2',
1723
+ 'Commit: 2, 2',
1724
+ ]);
1725
+
1726
+ // Now there are two pending mutations at different priorities. But they
1727
+ // both read the same version of the mutable source, so we must render
1728
+ // them simultaneously.
1729
+ //
1730
+ expect(Scheduler).toFlushAndYieldThrough([
1731
+ 'Parent: 3',
1732
+ // Demonstrates that we can yield here
1733
+ ]);
1734
+ expect(Scheduler).toFlushAndYield([
1735
+ // Now finish the rest of the update
1736
+ 'Child: 3',
1737
+ 'Commit: 3, 3',
1738
+ ]);
1739
+ }
1740
});
1741
});
1742
@@ -1855,22 +1971,43 @@ describe('useMutableSource', () => {
1971
1972
act(() => {
1973
// Start a render that uses the mutable source.
1858
- ReactNoop.render(
1859
- <>
1860
- <Component
1861
- label="a"
1862
- getSnapshot={defaultGetSnapshot}
1863
- mutableSource={mutableSource}
1864
- subscribe={defaultSubscribe}
1865
- />
1866
- <Component
1867
- label="b"
1868
- getSnapshot={defaultGetSnapshot}
1869
- mutableSource={mutableSource}
1870
- subscribe={defaultSubscribe}
1871
- />
1872
- </>,
1873
- );
1974
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1975
+ React.unstable_startTransition(() => {
1976
+ ReactNoop.render(
1977
+ <>
1978
+ <Component
1979
+ label="a"
1980
+ getSnapshot={defaultGetSnapshot}
1981
+ mutableSource={mutableSource}
1982
+ subscribe={defaultSubscribe}
1983
+ />
1984
+ <Component
1985
+ label="b"
1986
+ getSnapshot={defaultGetSnapshot}
1987
+ mutableSource={mutableSource}
1988
+ subscribe={defaultSubscribe}
1989
+ />
1990
+ </>,
1991
+ );
1992
+ });
1993
+ } else {
1994
+ ReactNoop.render(
1995
+ <>
1996
+ <Component
1997
+ label="a"
1998
+ getSnapshot={defaultGetSnapshot}
1999
+ mutableSource={mutableSource}
2000
+ subscribe={defaultSubscribe}
2001
+ />
2002
+ <Component
2003
+ label="b"
2004
+ getSnapshot={defaultGetSnapshot}
2005
+ mutableSource={mutableSource}
2006
+ subscribe={defaultSubscribe}
2007
+ />
2008
+ </>,
2009
+ );
2010
+ }
2011
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
2012
2013
const PrevScheduler = Scheduler;
@@ -1915,22 +2052,43 @@ describe('useMutableSource', () => {
2052
2053
act(() => {
2054
// Start a render that uses the mutable source.
1918
- ReactNoop.render(
1919
- <>
1920
- <Component
1921
- label="a"
1922
- getSnapshot={defaultGetSnapshot}
1923
- mutableSource={mutableSource}
1924
- subscribe={defaultSubscribe}
1925
- />
1926
- <Component
1927
- label="b"
1928
- getSnapshot={defaultGetSnapshot}
1929
- mutableSource={mutableSource}
1930
- subscribe={defaultSubscribe}
1931
- />
1932
- </>,
1933
- );
2055
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
2056
+ React.unstable_startTransition(() => {
2057
+ ReactNoop.render(
2058
+ <>
2059
+ <Component
2060
+ label="a"
2061
+ getSnapshot={defaultGetSnapshot}
2062
+ mutableSource={mutableSource}
2063
+ subscribe={defaultSubscribe}
2064
+ />
2065
+ <Component
2066
+ label="b"
2067
+ getSnapshot={defaultGetSnapshot}
2068
+ mutableSource={mutableSource}
2069
+ subscribe={defaultSubscribe}
2070
+ />
2071
+ </>,
2072
+ );
2073
+ });
2074
+ } else {
2075
+ ReactNoop.render(
2076
+ <>
2077
+ <Component
2078
+ label="a"
2079
+ getSnapshot={defaultGetSnapshot}
2080
+ mutableSource={mutableSource}
2081
+ subscribe={defaultSubscribe}
2082
+ />
2083
+ <Component
2084
+ label="b"
2085
+ getSnapshot={defaultGetSnapshot}
2086
+ mutableSource={mutableSource}
2087
+ subscribe={defaultSubscribe}
2088
+ />
2089
+ </>,
2090
+ );
2091
+ }
2092
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
2093
2094
const PrevScheduler = Scheduler;
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+52
-18
@@ -262,7 +262,13 @@ describe('useMutableSourceHydration', () => {
262
});
263
expect(() => {
264
act(() => {
265
- root.render(<TestComponent />);
265
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
266
+ React.unstable_startTransition(() => {
267
+ root.render(<TestComponent />);
268
+ });
269
+ } else {
270
+ root.render(<TestComponent />);
271
+ }
272
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
273
source.value = 'two';
274
});
@@ -316,22 +322,43 @@ describe('useMutableSourceHydration', () => {
322
});
323
expect(() => {
324
act(() => {
319
- root.render(
320
- <>
321
- <Component
322
- label="0"
323
- getSnapshot={getSnapshotA}
324
- mutableSource={mutableSource}
325
- subscribe={subscribeA}
326
- />
327
- <Component
328
- label="1"
329
- getSnapshot={getSnapshotB}
330
- mutableSource={mutableSource}
331
- subscribe={subscribeB}
332
- />
333
- </>,
334
- );
325
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
326
+ React.unstable_startTransition(() => {
327
+ root.render(
328
+ <>
329
+ <Component
330
+ label="0"
331
+ getSnapshot={getSnapshotA}
332
+ mutableSource={mutableSource}
333
+ subscribe={subscribeA}
334
+ />
335
+ <Component
336
+ label="1"
337
+ getSnapshot={getSnapshotB}
338
+ mutableSource={mutableSource}
339
+ subscribe={subscribeB}
340
+ />
341
+ </>,
342
+ );
343
+ });
344
+ } else {
345
+ root.render(
346
+ <>
347
+ <Component
348
+ label="0"
349
+ getSnapshot={getSnapshotA}
350
+ mutableSource={mutableSource}
351
+ subscribe={subscribeA}
352
+ />
353
+ <Component
354
+ label="1"
355
+ getSnapshot={getSnapshotB}
356
+ mutableSource={mutableSource}
357
+ subscribe={subscribeB}
358
+ />
359
+ </>,
360
+ );
361
+ }
362
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
363
source.valueB = 'b:two';
364
});
@@ -344,6 +371,7 @@ describe('useMutableSourceHydration', () => {
371
});
372
373
// @gate experimental
374
+ // @gate !enableSyncDefaultUpdates
375
it('should detect a tear during a higher priority interruption', () => {
376
const source = createSource('one');
377
const mutableSource = createMutableSource(source, param => param.version);
@@ -386,7 +414,13 @@ describe('useMutableSourceHydration', () => {
414
415
expect(() => {
416
act(() => {
389
- root.render(<TestComponent flag={1} />);
417
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
418
+ React.unstable_startTransition(() => {
419
+ root.render(<TestComponent flag={1} />);
420
+ });
421
+ } else {
422
+ root.render(<TestComponent flag={1} />);
423
+ }
424
expect(Scheduler).toFlushAndYieldThrough([1]);
425
426
// Render an update which will be higher priority than the hydration.
packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+27
-6
@@ -73,6 +73,7 @@ describe('ReactTestRendererAsync', () => {
73
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
74
});
75
76
+ // @gate experimental || !enableSyncDefaultUpdates
77
it('flushThrough flushes until the expected values is yielded', () => {
78
function Child(props) {
79
Scheduler.unstable_yieldValue(props.children);
@@ -87,9 +88,19 @@ describe('ReactTestRendererAsync', () => {
88
</>
89
);
90
}
90
- const renderer = ReactTestRenderer.create(<Parent step={1} />, {
91
- unstable_isConcurrent: true,
92
- });
91
+
92
+ let renderer;
93
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
94
+ React.unstable_startTransition(() => {
95
+ renderer = ReactTestRenderer.create(<Parent step={1} />, {
96
+ unstable_isConcurrent: true,
97
+ });
98
+ });
99
+ } else {
100
+ renderer = ReactTestRenderer.create(<Parent step={1} />, {
101
+ unstable_isConcurrent: true,
102
+ });
103
+ }
104
105
// Flush the first two siblings
106
expect(Scheduler).toFlushAndYieldThrough(['A:1', 'B:1']);
@@ -101,6 +112,7 @@ describe('ReactTestRendererAsync', () => {
112
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
113
});
114
115
+ // @gate experimental || !enableSyncDefaultUpdates
116
it('supports high priority interruptions', () => {
117
function Child(props) {
118
Scheduler.unstable_yieldValue(props.children);
@@ -124,9 +136,18 @@ describe('ReactTestRendererAsync', () => {
136
}
137
}
138
127
- const renderer = ReactTestRenderer.create(<Example step={1} />, {
128
- unstable_isConcurrent: true,
129
- });
139
+ let renderer;
140
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
141
+ React.unstable_startTransition(() => {
142
+ renderer = ReactTestRenderer.create(<Example step={1} />, {
143
+ unstable_isConcurrent: true,
144
+ });
145
+ });
146
+ } else {
147
+ renderer = ReactTestRenderer.create(<Example step={1} />, {
148
+ unstable_isConcurrent: true,
149
+ });
150
+ }
151
152
// Flush the some of the changes, but don't commit
153
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+52
-22
@@ -123,11 +123,21 @@ describe('ReactDOMTracing', () => {
123
SchedulerTracing.unstable_trace('initialization', 0, () => {
124
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
125
act(() => {
126
- root.render(
127
- <React.Profiler id="test" onRender={onRender}>
128
- <App />
129
- </React.Profiler>,
130
- );
126
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
127
+ React.unstable_startTransition(() => {
128
+ root.render(
129
+ <React.Profiler id="test" onRender={onRender}>
130
+ <App />
131
+ </React.Profiler>,
132
+ );
133
+ });
134
+ } else {
135
+ root.render(
136
+ <React.Profiler id="test" onRender={onRender}>
137
+ <App />
138
+ </React.Profiler>,
139
+ );
140
+ }
141
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
142
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
143
interaction,
@@ -140,6 +150,7 @@ describe('ReactDOMTracing', () => {
150
);
151
expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
152
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
153
+
154
expect(onRender).toHaveBeenCalledTimes(2);
155
expect(onRender).toHaveLastRenderedWithInteractions(
156
new Set([interaction]),
@@ -275,11 +286,21 @@ describe('ReactDOMTracing', () => {
286
SchedulerTracing.unstable_trace('initialization', 0, () => {
287
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
288
act(() => {
278
- root.render(
279
- <React.Profiler id="test" onRender={onRender}>
280
- <App />
281
- </React.Profiler>,
282
- );
289
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
290
+ React.unstable_startTransition(() => {
291
+ root.render(
292
+ <React.Profiler id="test" onRender={onRender}>
293
+ <App />
294
+ </React.Profiler>,
295
+ );
296
+ });
297
+ } else {
298
+ root.render(
299
+ <React.Profiler id="test" onRender={onRender}>
300
+ <App />
301
+ </React.Profiler>,
302
+ );
303
+ }
304
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
305
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
306
interaction,
@@ -307,16 +328,13 @@ describe('ReactDOMTracing', () => {
328
expect(
329
onInteractionScheduledWorkCompleted,
330
).toHaveBeenLastNotifiedOfInteraction(interaction);
310
- if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) {
311
- expect(onRender).toHaveBeenCalledTimes(3);
312
- } else {
313
- // TODO: This is 4 instead of 3 because this update was scheduled at
314
- // idle priority, and idle updates are slightly higher priority than
315
- // offscreen work. So it takes two render passes to finish it. Profiler
316
- // calls `onRender` for the first render even though everything
317
- // bails out.
318
- expect(onRender).toHaveBeenCalledTimes(4);
319
- }
331
+
332
+ // TODO: This is 4 instead of 3 because this update was scheduled at
333
+ // idle priority, and idle updates are slightly higher priority than
334
+ // offscreen work. So it takes two render passes to finish it. Profiler
335
+ // calls `onRender` for the first render even though everything
336
+ // bails out.
337
+ expect(onRender).toHaveBeenCalledTimes(4);
338
expect(onRender).toHaveLastRenderedWithInteractions(
339
new Set([interaction]),
340
);
@@ -503,7 +521,13 @@ describe('ReactDOMTracing', () => {
521
scheduleUpdateWithHidden(),
522
);
523
});
506
- scheduleUpdate();
524
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
525
+ React.unstable_startTransition(() => {
526
+ scheduleUpdate();
527
+ });
528
+ } else {
529
+ scheduleUpdate();
530
+ }
531
expect(interaction).not.toBeNull();
532
expect(onRender).toHaveBeenCalledTimes(1);
533
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -580,7 +604,13 @@ describe('ReactDOMTracing', () => {
604
SchedulerTracing.unstable_trace('initialization', 0, () => {
605
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
606
// This render is only CPU bound. Nothing suspends.
583
- root.render(<App />);
607
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
608
+ React.unstable_startTransition(() => {
609
+ root.render(<App />);
610
+ });
611
+ } else {
612
+ root.render(<App />);
613
+ }
614
});
615
616
expect(Scheduler).toFlushAndYieldThrough(['A']);
packages/react/src/__tests__/ReactProfiler-test.internal.js
+162
-53
@@ -246,6 +246,7 @@ describe('Profiler', () => {
246
expect(callback).toHaveBeenCalledTimes(2);
247
});
248
249
+ // @gate experimental || !enableSyncDefaultUpdates
250
it('is not invoked until the commit phase', () => {
251
const callback = jest.fn();
252
@@ -254,15 +255,29 @@ describe('Profiler', () => {
255
return null;
256
};
257
257
- ReactTestRenderer.create(
258
- <React.Profiler id="test" onRender={callback}>
259
- <Yield value="first" />
260
- <Yield value="last" />
261
- </React.Profiler>,
262
- {
263
- unstable_isConcurrent: true,
264
- },
265
- );
258
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
259
+ React.unstable_startTransition(() => {
260
+ ReactTestRenderer.create(
261
+ <React.Profiler id="test" onRender={callback}>
262
+ <Yield value="first" />
263
+ <Yield value="last" />
264
+ </React.Profiler>,
265
+ {
266
+ unstable_isConcurrent: true,
267
+ },
268
+ );
269
+ });
270
+ } else {
271
+ ReactTestRenderer.create(
272
+ <React.Profiler id="test" onRender={callback}>
273
+ <Yield value="first" />
274
+ <Yield value="last" />
275
+ </React.Profiler>,
276
+ {
277
+ unstable_isConcurrent: true,
278
+ },
279
+ );
280
+ }
281
282
// Times are logged until a render is committed.
283
expect(Scheduler).toFlushAndYieldThrough(['first']);
@@ -796,6 +811,7 @@ describe('Profiler', () => {
811
});
812
813
describe('with regard to interruptions', () => {
814
+ // @gate experimental || !enableSyncDefaultUpdates
815
it('should accumulate actual time after a scheduling interruptions', () => {
816
const callback = jest.fn();
817
@@ -808,13 +824,25 @@ describe('Profiler', () => {
824
Scheduler.unstable_advanceTime(5); // 0 -> 5
825
826
// Render partially, but run out of time before completing.
811
- ReactTestRenderer.create(
812
- <React.Profiler id="test" onRender={callback}>
813
- <Yield renderTime={2} />
814
- <Yield renderTime={3} />
815
- </React.Profiler>,
816
- {unstable_isConcurrent: true},
817
- );
827
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
828
+ React.unstable_startTransition(() => {
829
+ ReactTestRenderer.create(
830
+ <React.Profiler id="test" onRender={callback}>
831
+ <Yield renderTime={2} />
832
+ <Yield renderTime={3} />
833
+ </React.Profiler>,
834
+ {unstable_isConcurrent: true},
835
+ );
836
+ });
837
+ } else {
838
+ ReactTestRenderer.create(
839
+ <React.Profiler id="test" onRender={callback}>
840
+ <Yield renderTime={2} />
841
+ <Yield renderTime={3} />
842
+ </React.Profiler>,
843
+ {unstable_isConcurrent: true},
844
+ );
845
+ }
846
expect(Scheduler).toFlushAndYieldThrough(['Yield:2']);
847
expect(callback).toHaveBeenCalledTimes(0);
848
@@ -830,6 +858,7 @@ describe('Profiler', () => {
858
expect(call[5]).toBe(10); // commit time
859
});
860
861
+ // @gate experimental || !enableSyncDefaultUpdates
862
it('should not include time between frames', () => {
863
const callback = jest.fn();
864
@@ -843,16 +872,31 @@ describe('Profiler', () => {
872
873
// Render partially, but don't finish.
874
// This partial render should take 5ms of simulated time.
846
- ReactTestRenderer.create(
847
- <React.Profiler id="outer" onRender={callback}>
848
- <Yield renderTime={5} />
849
- <Yield renderTime={10} />
850
- <React.Profiler id="inner" onRender={callback}>
851
- <Yield renderTime={17} />
852
- </React.Profiler>
853
- </React.Profiler>,
854
- {unstable_isConcurrent: true},
855
- );
875
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
876
+ React.unstable_startTransition(() => {
877
+ ReactTestRenderer.create(
878
+ <React.Profiler id="outer" onRender={callback}>
879
+ <Yield renderTime={5} />
880
+ <Yield renderTime={10} />
881
+ <React.Profiler id="inner" onRender={callback}>
882
+ <Yield renderTime={17} />
883
+ </React.Profiler>
884
+ </React.Profiler>,
885
+ {unstable_isConcurrent: true},
886
+ );
887
+ });
888
+ } else {
889
+ ReactTestRenderer.create(
890
+ <React.Profiler id="outer" onRender={callback}>
891
+ <Yield renderTime={5} />
892
+ <Yield renderTime={10} />
893
+ <React.Profiler id="inner" onRender={callback}>
894
+ <Yield renderTime={17} />
895
+ </React.Profiler>
896
+ </React.Profiler>,
897
+ {unstable_isConcurrent: true},
898
+ );
899
+ }
900
expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
901
expect(callback).toHaveBeenCalledTimes(0);
902
@@ -880,6 +924,7 @@ describe('Profiler', () => {
924
expect(outerCall[5]).toBe(87); // commit time
925
});
926
927
+ // @gate experimental || !enableSyncDefaultUpdates
928
it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
929
const callback = jest.fn();
930
@@ -893,13 +938,26 @@ describe('Profiler', () => {
938
939
// Render a partially update, but don't finish.
940
// This partial render should take 10ms of simulated time.
896
- const renderer = ReactTestRenderer.create(
897
- <React.Profiler id="test" onRender={callback}>
898
- <Yield renderTime={10} />
899
- <Yield renderTime={20} />
900
- </React.Profiler>,
901
- {unstable_isConcurrent: true},
902
- );
941
+ let renderer;
942
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
943
+ React.unstable_startTransition(() => {
944
+ renderer = ReactTestRenderer.create(
945
+ <React.Profiler id="test" onRender={callback}>
946
+ <Yield renderTime={10} />
947
+ <Yield renderTime={20} />
948
+ </React.Profiler>,
949
+ {unstable_isConcurrent: true},
950
+ );
951
+ });
952
+ } else {
953
+ renderer = ReactTestRenderer.create(
954
+ <React.Profiler id="test" onRender={callback}>
955
+ <Yield renderTime={10} />
956
+ <Yield renderTime={20} />
957
+ </React.Profiler>,
958
+ {unstable_isConcurrent: true},
959
+ );
960
+ }
961
expect(Scheduler).toFlushAndYieldThrough(['Yield:10']);
962
expect(callback).toHaveBeenCalledTimes(0);
963
@@ -933,6 +991,7 @@ describe('Profiler', () => {
991
expect(callback).toHaveBeenCalledTimes(0);
992
});
993
994
+ // @gate experimental || !enableSyncDefaultUpdates
995
it('should report the expected times when a high-priority update replaces a low-priority update', () => {
996
const callback = jest.fn();
997
@@ -968,13 +1027,25 @@ describe('Profiler', () => {
1027
1028
// Render a partially update, but don't finish.
1029
// This partial render should take 3ms of simulated time.
971
- renderer.update(
972
- <React.Profiler id="test" onRender={callback}>
973
- <Yield renderTime={3} />
974
- <Yield renderTime={5} />
975
- <Yield renderTime={9} />
976
- </React.Profiler>,
977
- );
1030
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1031
+ React.unstable_startTransition(() => {
1032
+ renderer.update(
1033
+ <React.Profiler id="test" onRender={callback}>
1034
+ <Yield renderTime={3} />
1035
+ <Yield renderTime={5} />
1036
+ <Yield renderTime={9} />
1037
+ </React.Profiler>,
1038
+ );
1039
+ });
1040
+ } else {
1041
+ renderer.update(
1042
+ <React.Profiler id="test" onRender={callback}>
1043
+ <Yield renderTime={3} />
1044
+ <Yield renderTime={5} />
1045
+ <Yield renderTime={9} />
1046
+ </React.Profiler>,
1047
+ );
1048
+ }
1049
expect(Scheduler).toFlushAndYieldThrough(['Yield:3']);
1050
expect(callback).toHaveBeenCalledTimes(0);
1051
@@ -1014,6 +1085,7 @@ describe('Profiler', () => {
1085
expect(callback).toHaveBeenCalledTimes(1);
1086
});
1087
1088
+ // @gate experimental || !enableSyncDefaultUpdates
1089
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
1090
const callback = jest.fn();
1091
@@ -1080,7 +1152,13 @@ describe('Profiler', () => {
1152
1153
// Render a partially update, but don't finish.
1154
// This partial render will take 10ms of actual render time.
1083
- first.setState({renderTime: 10});
1155
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
1156
+ React.unstable_startTransition(() => {
1157
+ first.setState({renderTime: 10});
1158
+ });
1159
+ } else {
1160
+ first.setState({renderTime: 10});
1161
+ }
1162
expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']);
1163
expect(callback).toHaveBeenCalledTimes(0);
1164
@@ -3343,6 +3421,7 @@ describe('Profiler', () => {
3421
).toHaveBeenLastNotifiedOfInteraction(interaction);
3422
});
3423
3424
+ // @gate experimental || !enableSyncDefaultUpdates
3425
it('should associate traced events with their subsequent commits', () => {
3426
let instance = null;
3427
@@ -3459,10 +3538,19 @@ describe('Profiler', () => {
3538
interactionOne.name,
3539
Scheduler.unstable_now(),
3540
() => {
3462
- instance.setState({count: 1});
3541
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
3542
+ React.unstable_startTransition(() => {
3543
+ instance.setState({count: 1});
3544
3464
- // Update state again to verify our traced interaction isn't registered twice
3465
- instance.setState({count: 2});
3545
+ // Update state again to verify our traced interaction isn't registered twice
3546
+ instance.setState({count: 2});
3547
+ });
3548
+ } else {
3549
+ instance.setState({count: 1});
3550
+
3551
+ // Update state again to verify our traced interaction isn't registered twice
3552
+ instance.setState({count: 2});
3553
+ }
3554
3555
// The scheduler/tracing package will notify of work started for the default thread,
3556
// But React shouldn't notify until it's been flushed.
@@ -3655,6 +3743,7 @@ describe('Profiler', () => {
3743
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3744
});
3745
3746
+ // @gate experimental || !enableSyncDefaultUpdates
3747
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
3748
const onPostCommit = jest.fn(() => {
3749
Scheduler.unstable_yieldValue('onPostCommit');
@@ -3715,7 +3804,13 @@ describe('Profiler', () => {
3804
Scheduler.unstable_now(),
3805
() => {
3806
// Render a partially update, but don't finish.
3718
- first.setState({count: 1});
3807
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
3808
+ React.unstable_startTransition(() => {
3809
+ first.setState({count: 1});
3810
+ });
3811
+ } else {
3812
+ first.setState({count: 1});
3813
+ }
3814
3815
expect(onWorkScheduled).toHaveBeenCalled();
3816
expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
@@ -4590,6 +4685,7 @@ describe('Profiler', () => {
4685
).toMatchInteraction(initialRenderInteraction);
4686
});
4687
4688
+ // @gate experimental || !enableSyncDefaultUpdates
4689
it('handles high-pri renderers between suspended and resolved (async) trees', async () => {
4690
// Set up an initial shell. We need to set this up before the test sceanrio
4691
// because we want initial render to suspend on navigation to the initial state.
@@ -4614,14 +4710,27 @@ describe('Profiler', () => {
4710
initialRenderInteraction.name,
4711
initialRenderInteraction.timestamp,
4712
() => {
4617
- renderer.update(
4618
- <React.Profiler id="app" onCommit={onCommit}>
4619
- <React.Suspense fallback={<Text text="loading" />}>
4620
- <AsyncText text="loaded" ms={100} />
4621
- </React.Suspense>
4622
- <Text text="initial" />
4623
- </React.Profiler>,
4624
- );
4713
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
4714
+ React.unstable_startTransition(() => {
4715
+ renderer.update(
4716
+ <React.Profiler id="app" onCommit={onCommit}>
4717
+ <React.Suspense fallback={<Text text="loading" />}>
4718
+ <AsyncText text="loaded" ms={100} />
4719
+ </React.Suspense>
4720
+ <Text text="initial" />
4721
+ </React.Profiler>,
4722
+ );
4723
+ });
4724
+ } else {
4725
+ renderer.update(
4726
+ <React.Profiler id="app" onCommit={onCommit}>
4727
+ <React.Suspense fallback={<Text text="loading" />}>
4728
+ <AsyncText text="loaded" ms={100} />
4729
+ </React.Suspense>
4730
+ <Text text="initial" />
4731
+ </React.Profiler>,
4732
+ );
4733
+ }
4734
},
4735
);
4736
expect(Scheduler).toFlushAndYield([
packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
+8
-1
@@ -178,6 +178,7 @@ describe('ReactProfiler DevTools integration', () => {
178
]);
179
});
180
181
+ // @gate experimental || !enableSyncDefaultUpdates
182
it('regression test: #17159', () => {
183
function Text({text}) {
184
Scheduler.unstable_yieldValue(text);
@@ -195,7 +196,13 @@ describe('ReactProfiler DevTools integration', () => {
196
// for updates.
197
Scheduler.unstable_advanceTime(10000);
198
// Schedule an update.
198
- root.update(<Text text="B" />);
199
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
200
+ React.unstable_startTransition(() => {
201
+ root.update(<Text text="B" />);
202
+ });
203
+ } else {
204
+ root.update(<Text text="B" />);
205
+ }
206
207
// Update B should not instantly expire.
208
expect(Scheduler).toFlushAndYieldThrough([]);
packages/shared/ReactFeatureFlags.js
+2
@@ -170,3 +170,5 @@ export const enableRecursiveCommitTraversal = false;
170
export const disableSchedulerTimeoutInWorkLoop = false;
171
172
export const enableLazyContextPropagation = false;
173
+
174
+export const enableSyncDefaultUpdates = true;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -60,6 +60,7 @@ export const enableUseRefAccessWarning = false;
60
export const enableRecursiveCommitTraversal = false;
61
export const disableSchedulerTimeoutInWorkLoop = false;
62
export const enableLazyContextPropagation = false;
63
+export const enableSyncDefaultUpdates = true;
64
65
// Flow magic to verify the exports of this file match the original version.
66
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
+export const enableSyncDefaultUpdates = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -59,3 +59,4 @@ export const deletedTreeCleanUpLevel = __VARIANT__ ? 3 : 1;
59
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
60
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
61
export const enableLazyContextPropagation = __VARIANT__;
62
+export const enableSyncDefaultUpdates = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -34,6 +34,7 @@ export const {
34
disableSchedulerTimeoutInWorkLoop,
35
enableLazyContextPropagation,
36
deletedTreeCleanUpLevel,
37
+ enableSyncDefaultUpdates,
38
} = dynamicFeatureFlags;
39
40
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
packages/use-subscription/src/__tests__/useSubscription-test.js
+32
-4
@@ -262,6 +262,7 @@ describe('useSubscription', () => {
262
expect(subscriptions).toHaveLength(2);
263
});
264
265
+ // @gate experimental || !enableSyncDefaultUpdates
266
it('should ignore values emitted by a new subscribable until the commit phase', () => {
267
const log = [];
268
@@ -331,7 +332,14 @@ describe('useSubscription', () => {
332
333
// Start React update, but don't finish
334
act(() => {
334
- renderer.update(<Parent observed={observableB} />);
335
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
336
+ React.unstable_startTransition(() => {
337
+ renderer.update(<Parent observed={observableB} />);
338
+ });
339
+ } else {
340
+ renderer.update(<Parent observed={observableB} />);
341
+ }
342
+
343
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
344
expect(log).toEqual(['Parent.componentDidMount']);
345
@@ -362,6 +370,7 @@ describe('useSubscription', () => {
370
]);
371
});
372
373
+ // @gate experimental || !enableSyncDefaultUpdates
374
it('should not drop values emitted between updates', () => {
375
const log = [];
376
@@ -432,7 +441,13 @@ describe('useSubscription', () => {
441
442
// Start React update, but don't finish
443
act(() => {
435
- renderer.update(<Parent observed={observableB} />);
444
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
445
+ React.unstable_startTransition(() => {
446
+ renderer.update(<Parent observed={observableB} />);
447
+ });
448
+ } else {
449
+ renderer.update(<Parent observed={observableB} />);
450
+ }
451
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
452
expect(log).toEqual([]);
453
@@ -561,6 +576,7 @@ describe('useSubscription', () => {
576
Scheduler.unstable_flushAll();
577
});
578
579
+ // @gate experimental || !enableSyncDefaultUpdates
580
it('should not tear if a mutation occurs during a concurrent update', () => {
581
const input = document.createElement('input');
582
@@ -608,9 +624,21 @@ describe('useSubscription', () => {
624
// Interrupt with a second mutation "C" -> "D".
625
// This update will not be eagerly evaluated,
626
// but useSubscription() should eagerly close over the updated value to avoid tearing.
611
- mutate('C');
627
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
628
+ React.unstable_startTransition(() => {
629
+ mutate('C');
630
+ });
631
+ } else {
632
+ mutate('C');
633
+ }
634
expect(Scheduler).toFlushAndYieldThrough(['render:first:C']);
613
- mutate('D');
635
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
636
+ React.unstable_startTransition(() => {
637
+ mutate('D');
638
+ });
639
+ } else {
640
+ mutate('D');
641
+ }
642
expect(Scheduler).toFlushAndYield([
643
'render:second:C',
644
'render:first:D',