@samitouri / QOS-React-2 / commits / da94e8b24a

Revert "Cleanup enableSyncDefaultUpdate flag (#26236)" (#26528)

This reverts commit b2ae9ddb3b497d16a7c27c051da1827d08871138. While the feature flag is fully rolled out, these tests are also testing behavior set with an unstable flag on root, which for now we want to preserve. Not sure if there's a better way then adding a dynamic feature flag to the www build?

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