@samitouri / QOS-React / commits / 933880b454

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',