@samitouri / QOS-React-2 / commits / 5379b6123f

Batch sync, default and continuous lanes (#25700)

<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> This is the other approach for unifying default and sync lane https://github.com/facebook/react/pull/25524. The approach in that PR is to merge default and continuous lane into the sync lane, and use a new field to track the priority. But there are a couple places that field will be needed, and it is difficult to correctly reset the field when there is no sync lane. In this PR we take the other approach that doesn't remove any lane, but batch them to get the behavior we want. ## How did you test this change? <!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. How exactly did you verify that your PR solves the issue you wanted to solve? If you leave this empty, your PR will very likely be closed. --> yarn test Co-authored-by: Andrew Clark <hi@andrewclark.io>

Tianyu Yao committed Jan 5, 2023 at 15:21 UTC 5379b6123f171bb48cc8a9c435c11ccb9f8ff0e7
26 files changed +414 -267
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+21 -6
@@ -275,17 +275,32 @@ describe('ReactDOMFiberAsync', () => {
275 expect(ops).toEqual([]);
276 });
277 // Only the active updates have flushed
278 - expect(container.textContent).toEqual('BC');
279 - expect(ops).toEqual(['BC']);
278 + if (gate(flags => flags.enableUnifiedSyncLane)) {
279 + expect(container.textContent).toEqual('ABC');
280 + expect(ops).toEqual(['ABC']);
281 + } else {
282 + expect(container.textContent).toEqual('BC');
283 + expect(ops).toEqual(['BC']);
284 + }
285
281 - instance.push('D');
282 - expect(container.textContent).toEqual('BC');
283 - expect(ops).toEqual(['BC']);
286 + if (gate(flags => flags.enableUnifiedSyncLane)) {
287 + instance.push('D');
288 + expect(container.textContent).toEqual('ABC');
289 + expect(ops).toEqual(['ABC']);
290 + } else {
291 + instance.push('D');
292 + expect(container.textContent).toEqual('BC');
293 + expect(ops).toEqual(['BC']);
294 + }
295
296 // Flush the async updates
297 Scheduler.unstable_flushAll();
298 expect(container.textContent).toEqual('ABCD');
288 - expect(ops).toEqual(['BC', 'ABCD']);
299 + if (gate(flags => flags.enableUnifiedSyncLane)) {
300 + expect(ops).toEqual(['ABC', 'ABCD']);
301 + } else {
302 + expect(ops).toEqual(['BC', 'ABCD']);
303 + }
304 });
305
306 // @gate www
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+56 -64
@@ -449,10 +449,9 @@ describe('ReactDOMServerPartialHydration', () => {
449 expect(deleted.length).toBe(0);
450
451 // Performing an update should force it to delete the boundary
452 - root.render(<App value={true} />);
453 -
454 - Scheduler.unstable_flushAll();
455 - jest.runAllTimers();
452 + await act(async () => {
453 + root.render(<App value={true} />);
454 + });
455
456 expect(hydrated.length).toBe(1);
457 expect(deleted.length).toBe(1);
@@ -945,13 +944,12 @@ describe('ReactDOMServerPartialHydration', () => {
944 root.render(<App text="Hi" className="hi" />);
945
946 // At the same time, resolving the promise so that rendering can complete.
948 - suspend = false;
949 - resolve();
950 - await promise;
951 -
947 // This should first complete the hydration and then flush the update onto the hydrated state.
953 - Scheduler.unstable_flushAll();
954 - jest.runAllTimers();
948 + await act(async () => {
949 + suspend = false;
950 + resolve();
951 + await promise;
952 + });
953
954 // The new span should be the same since we should have successfully hydrated
955 // before changing it.
@@ -1093,9 +1091,9 @@ describe('ReactDOMServerPartialHydration', () => {
1091 expect(ref.current).toBe(null);
1092
1093 // Render an update, but leave it still suspended.
1096 - root.render(<App text="Hi" className="hi" />);
1097 - Scheduler.unstable_flushAll();
1098 - jest.runAllTimers();
1094 + await act(async () => {
1095 + root.render(<App text="Hi" className="hi" />);
1096 + });
1097
1098 // Flushing now should delete the existing content and show the fallback.
1099
@@ -1104,12 +1102,11 @@ describe('ReactDOMServerPartialHydration', () => {
1102 expect(container.textContent).toBe('Loading...');
1103
1104 // Unsuspending shows the content.
1107 - suspend = false;
1108 - resolve();
1109 - await promise;
1110 -
1111 - Scheduler.unstable_flushAll();
1112 - jest.runAllTimers();
1105 + await act(async () => {
1106 + suspend = false;
1107 + resolve();
1108 + await promise;
1109 + });
1110
1111 const span = container.getElementsByTagName('span')[0];
1112 expect(span.textContent).toBe('Hi');
@@ -1174,23 +1171,21 @@ describe('ReactDOMServerPartialHydration', () => {
1171 expect(ref.current).toBe(span);
1172
1173 // Render an update, but leave it still suspended.
1177 - root.render(<App text="Hi" className="hi" />);
1178 -
1174 // Flushing now should delete the existing content and show the fallback.
1180 - Scheduler.unstable_flushAll();
1181 - jest.runAllTimers();
1175 + await act(async () => {
1176 + root.render(<App text="Hi" className="hi" />);
1177 + });
1178
1179 expect(container.getElementsByTagName('span').length).toBe(1);
1180 expect(ref.current).toBe(span);
1181 expect(container.textContent).toBe('');
1182
1183 // Unsuspending shows the content.
1188 - suspend = false;
1189 - resolve();
1190 - await promise;
1191 -
1192 - Scheduler.unstable_flushAll();
1193 - jest.runAllTimers();
1184 + await act(async () => {
1185 + suspend = false;
1186 + resolve();
1187 + await promise;
1188 + });
1189
1190 expect(span.textContent).toBe('Hi');
1191 expect(span.className).toBe('hi');
@@ -1252,20 +1247,21 @@ describe('ReactDOMServerPartialHydration', () => {
1247 expect(ref.current).toBe(null);
1248
1249 // Render an update, but leave it still suspended.
1255 - root.render(<App text="Hi" className="hi" />);
1256 -
1250 // Flushing now should delete the existing content and show the fallback.
1258 - Scheduler.unstable_flushAll();
1259 - jest.runAllTimers();
1251 + await act(async () => {
1252 + root.render(<App text="Hi" className="hi" />);
1253 + });
1254
1255 expect(container.getElementsByTagName('span').length).toBe(0);
1256 expect(ref.current).toBe(null);
1257 expect(container.textContent).toBe('Loading...');
1258
1259 // Unsuspending shows the content.
1266 - suspend = false;
1267 - resolve();
1268 - await promise;
1260 + await act(async () => {
1261 + suspend = false;
1262 + resolve();
1263 + await promise;
1264 + });
1265
1266 Scheduler.unstable_flushAll();
1267 jest.runAllTimers();
@@ -1490,13 +1486,12 @@ describe('ReactDOMServerPartialHydration', () => {
1486 );
1487
1488 // At the same time, resolving the promise so that rendering can complete.
1493 - suspend = false;
1494 - resolve();
1495 - await promise;
1496 -
1489 // This should first complete the hydration and then flush the update onto the hydrated state.
1498 - Scheduler.unstable_flushAll();
1499 - jest.runAllTimers();
1490 + await act(async () => {
1491 + suspend = false;
1492 + resolve();
1493 + await promise;
1494 + });
1495
1496 // Since this should have been hydrated, this should still be the same span.
1497 const newSpan = container.getElementsByTagName('span')[0];
@@ -1569,27 +1564,25 @@ describe('ReactDOMServerPartialHydration', () => {
1564 expect(ref.current).toBe(null);
1565
1566 // Render an update, but leave it still suspended.
1572 - root.render(
1573 - <Context.Provider value={{text: 'Hi', className: 'hi'}}>
1574 - <App />
1575 - </Context.Provider>,
1576 - );
1577 -
1567 // Flushing now should delete the existing content and show the fallback.
1579 - Scheduler.unstable_flushAll();
1580 - jest.runAllTimers();
1568 + await act(async () => {
1569 + root.render(
1570 + <Context.Provider value={{text: 'Hi', className: 'hi'}}>
1571 + <App />
1572 + </Context.Provider>,
1573 + );
1574 + });
1575
1576 expect(container.getElementsByTagName('span').length).toBe(0);
1577 expect(ref.current).toBe(null);
1578 expect(container.textContent).toBe('Loading...');
1579
1580 // Unsuspending shows the content.
1587 - suspend = false;
1588 - resolve();
1589 - await promise;
1590 -
1591 - Scheduler.unstable_flushAll();
1592 - jest.runAllTimers();
1581 + await act(async () => {
1582 + suspend = false;
1583 + resolve();
1584 + await promise;
1585 + });
1586
1587 const span = container.getElementsByTagName('span')[0];
1588 expect(span.textContent).toBe('Hi');
@@ -2320,16 +2313,15 @@ describe('ReactDOMServerPartialHydration', () => {
2313
2314 // Render an update, which will be higher or the same priority as pinging the hydration.
2315 // The new update doesn't suspend.
2323 - root.render(
2324 - <ClassName.Provider value={'hi'}>
2325 - <App text="Hi" />
2326 - </ClassName.Provider>,
2327 - );
2328 -
2316 // Since we're still suspended on the original data, we can't hydrate.
2317 // This will force all expiration times to flush.
2331 - Scheduler.unstable_flushAll();
2332 - jest.runAllTimers();
2318 + await act(async () => {
2319 + root.render(
2320 + <ClassName.Provider value={'hi'}>
2321 + <App text="Hi" />
2322 + </ClassName.Provider>,
2323 + );
2324 + });
2325
2326 // This will now be a new span because we weren't able to hydrate before
2327 const newSpan = container.getElementsByTagName('span')[0];
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+15 -11
@@ -1786,7 +1786,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1786 document.body.removeChild(container);
1787 });
1788
1789 - it('can force hydration in response to sync update', () => {
1789 + it('can force hydration in response to sync update', async () => {
1790 function Child({text}) {
1791 Scheduler.unstable_yieldValue(`Child ${text}`);
1792 return <span ref={ref => (spanRef = ref)}>{text}</span>;
@@ -1812,15 +1812,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
1812 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1813 expect(Scheduler).toFlushUntilNextPaint(['App A']);
1814
1815 - ReactDOM.flushSync(() => {
1816 - root.render(<App text="B" />);
1815 + await act(async () => {
1816 + ReactDOM.flushSync(() => {
1817 + root.render(<App text="B" />);
1818 + });
1819 });
1820 expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
1821 expect(initialSpan).toBe(spanRef);
1822 });
1823
1824 // @gate experimental || www
1823 - it('can force hydration in response to continuous update', () => {
1825 + it('can force hydration in response to continuous update', async () => {
1826 function Child({text}) {
1827 Scheduler.unstable_yieldValue(`Child ${text}`);
1828 return <span ref={ref => (spanRef = ref)}>{text}</span>;
@@ -1846,14 +1848,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
1848 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1849 expect(Scheduler).toFlushUntilNextPaint(['App A']);
1850
1849 - TODO_scheduleContinuousSchedulerTask(() => {
1850 - root.render(<App text="B" />);
1851 + await act(async () => {
1852 + TODO_scheduleContinuousSchedulerTask(() => {
1853 + root.render(<App text="B" />);
1854 + });
1855 });
1852 - expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
1856 +
1857 + expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
1858 expect(initialSpan).toBe(spanRef);
1859 });
1860
1856 - it('can force hydration in response to default update', () => {
1861 + it('can force hydration in response to default update', async () => {
1862 function Child({text}) {
1863 Scheduler.unstable_yieldValue(`Child ${text}`);
1864 return <span ref={ref => (spanRef = ref)}>{text}</span>;
@@ -1878,11 +1883,10 @@ describe('ReactDOMServerSelectiveHydration', () => {
1883 const initialSpan = container.getElementsByTagName('span')[0];
1884 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1885 expect(Scheduler).toFlushUntilNextPaint(['App A']);
1881 -
1882 - ReactDOM.unstable_batchedUpdates(() => {
1886 + await act(async () => {
1887 root.render(<App text="B" />);
1888 });
1885 - expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
1889 + expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
1890 expect(initialSpan).toBe(spanRef);
1891 });
1892
packages/react-reconciler/src/ReactFiberLane.js
+53 -40
@@ -23,6 +23,7 @@ import {
23 enableUpdaterTracking,
24 allowConcurrentByDefault,
25 enableTransitionTracing,
26 + enableUnifiedSyncLane,
27 } from 'shared/ReactFeatureFlags';
28 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
29 import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
@@ -45,6 +46,8 @@ export const InputContinuousLane: Lane = /* */ 0b0000000000000000000
46 export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
47 export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
48
49 +export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;
50 +
51 const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
52 const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
53 const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
@@ -133,6 +136,12 @@ let nextTransitionLane: Lane = TransitionLane1;
136 let nextRetryLane: Lane = RetryLane1;
137
138 function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
139 + if (enableUnifiedSyncLane) {
140 + const pendingSyncLanes = lanes & SyncUpdateLanes;
141 + if (pendingSyncLanes !== 0) {
142 + return pendingSyncLanes;
143 + }
144 + }
145 switch (getHighestPriorityLane(lanes)) {
146 case SyncHydrationLane:
147 return SyncHydrationLane;
@@ -754,46 +763,50 @@ export function getBumpedLaneForHydration(
763 const renderLane = getHighestPriorityLane(renderLanes);
764
765 let lane;
757 - switch (renderLane) {
758 - case SyncLane:
759 - lane = SyncHydrationLane;
760 - break;
761 - case InputContinuousLane:
762 - lane = InputContinuousHydrationLane;
763 - break;
764 - case DefaultLane:
765 - lane = DefaultHydrationLane;
766 - break;
767 - case TransitionLane1:
768 - case TransitionLane2:
769 - case TransitionLane3:
770 - case TransitionLane4:
771 - case TransitionLane5:
772 - case TransitionLane6:
773 - case TransitionLane7:
774 - case TransitionLane8:
775 - case TransitionLane9:
776 - case TransitionLane10:
777 - case TransitionLane11:
778 - case TransitionLane12:
779 - case TransitionLane13:
780 - case TransitionLane14:
781 - case TransitionLane15:
782 - case TransitionLane16:
783 - case RetryLane1:
784 - case RetryLane2:
785 - case RetryLane3:
786 - case RetryLane4:
787 - lane = TransitionHydrationLane;
788 - break;
789 - case IdleLane:
790 - lane = IdleHydrationLane;
791 - break;
792 - default:
793 - // Everything else is already either a hydration lane, or shouldn't
794 - // be retried at a hydration lane.
795 - lane = NoLane;
796 - break;
766 + if (enableUnifiedSyncLane && (renderLane & SyncUpdateLanes) !== NoLane) {
767 + lane = SyncHydrationLane;
768 + } else {
769 + switch (renderLane) {
770 + case SyncLane:
771 + lane = SyncHydrationLane;
772 + break;
773 + case InputContinuousLane:
774 + lane = InputContinuousHydrationLane;
775 + break;
776 + case DefaultLane:
777 + lane = DefaultHydrationLane;
778 + break;
779 + case TransitionLane1:
780 + case TransitionLane2:
781 + case TransitionLane3:
782 + case TransitionLane4:
783 + case TransitionLane5:
784 + case TransitionLane6:
785 + case TransitionLane7:
786 + case TransitionLane8:
787 + case TransitionLane9:
788 + case TransitionLane10:
789 + case TransitionLane11:
790 + case TransitionLane12:
791 + case TransitionLane13:
792 + case TransitionLane14:
793 + case TransitionLane15:
794 + case TransitionLane16:
795 + case RetryLane1:
796 + case RetryLane2:
797 + case RetryLane3:
798 + case RetryLane4:
799 + lane = TransitionHydrationLane;
800 + break;
801 + case IdleLane:
802 + lane = IdleHydrationLane;
803 + break;
804 + default:
805 + // Everything else is already either a hydration lane, or shouldn't
806 + // be retried at a hydration lane.
807 + lane = NoLane;
808 + break;
809 + }
810 }
811
812 // Check if the lane we chose is suspended. If so, that indicates that we
packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
+12 -6
@@ -157,12 +157,18 @@ describe('ReactBlockingMode', () => {
157 }),
158 );
159
160 - // Only the second update should have flushed synchronously
161 - expect(Scheduler).toHaveYielded(['B1']);
162 - expect(root).toMatchRenderedOutput('A0B1');
163 -
160 // Now flush the first update
165 - expect(Scheduler).toFlushAndYield(['A1']);
166 - expect(root).toMatchRenderedOutput('A1B1');
161 + if (gate(flags => flags.enableUnifiedSyncLane)) {
162 + expect(Scheduler).toHaveYielded(['A1', 'B1']);
163 + expect(root).toMatchRenderedOutput('A1B1');
164 + } else {
165 + // Only the second update should have flushed synchronously
166 + expect(Scheduler).toHaveYielded(['B1']);
167 + expect(root).toMatchRenderedOutput('A0B1');
168 +
169 + // Now flush the first update
170 + expect(Scheduler).toFlushAndYield(['A1']);
171 + expect(root).toMatchRenderedOutput('A1B1');
172 + }
173 });
174 });
packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js
+11 -3
@@ -35,9 +35,17 @@ describe('ReactClassSetStateCallback', () => {
35 expect(Scheduler).toHaveYielded([0]);
36
37 await act(async () => {
38 - app.setState({step: 1}, () =>
39 - Scheduler.unstable_yieldValue('Callback 1'),
40 - );
38 + if (gate(flags => flags.enableUnifiedSyncLane)) {
39 + React.startTransition(() => {
40 + app.setState({step: 1}, () =>
41 + Scheduler.unstable_yieldValue('Callback 1'),
42 + );
43 + });
44 + } else {
45 + app.setState({step: 1}, () =>
46 + Scheduler.unstable_yieldValue('Callback 1'),
47 + );
48 + }
49 ReactNoop.flushSync(() => {
50 app.setState({step: 2}, () =>
51 Scheduler.unstable_yieldValue('Callback 2'),
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+3 -1
@@ -339,7 +339,9 @@ describe('ReactExpiration', () => {
339 // Before the update can finish, update again. Even though no time has
340 // advanced, this update should be given a different expiration time than
341 // the currently rendering one. So, C and D should render with 1, not 2.
342 - subscribers.forEach(s => s.setState({text: '2'}));
342 + React.startTransition(() => {
343 + subscribers.forEach(s => s.setState({text: '2'}));
344 + });
345 expect(Scheduler).toFlushAndYieldThrough([
346 '1 [C] [render]',
347 '1 [D] [render]',
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+9 -3
@@ -54,15 +54,21 @@ describe('ReactFlushSync', () => {
54 // The passive effect will schedule a sync update and a normal update.
55 // They should commit in two separate batches. First the sync one.
56 expect(() => {
57 - expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
57 + expect(Scheduler).toFlushUntilNextPaint(
58 + gate(flags => flags.enableUnifiedSyncLane) ? ['1, 1'] : ['1, 0'],
59 + );
60 }).toErrorDev('flushSync was called from inside a lifecycle method');
61
62 // The remaining update is not sync
63 ReactNoop.flushSync();
64 expect(Scheduler).toHaveYielded([]);
65
64 - // Now flush it.
65 - expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
66 + if (gate(flags => flags.enableUnifiedSyncLane)) {
67 + expect(Scheduler).toFlushUntilNextPaint([]);
68 + } else {
69 + // Now flush it.
70 + expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
71 + }
72 });
73 expect(root).toMatchRenderedOutput('1, 1');
74 });
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+7 -3
@@ -568,9 +568,13 @@ describe('ReactHooks', () => {
568 });
569 };
570
571 - // Update at normal priority
572 - ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
573 -
571 + if (gate(flags => flags.enableUnifiedSyncLane)) {
572 + // Update at transition priority
573 + React.startTransition(() => update(n => n * 100));
574 + } else {
575 + // Update at normal priority
576 + ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
577 + }
578 // The new state is eagerly computed.
579 expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
580
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+25 -11
@@ -815,7 +815,13 @@ describe('ReactHooksWithNoopRenderer', () => {
815 ReactNoop.discreteUpdates(() => {
816 setRow(5);
817 });
818 - setRow(20);
818 + if (gate(flags => flags.enableSyncDefaultUpdates)) {
819 + React.startTransition(() => {
820 + setRow(20);
821 + });
822 + } else {
823 + setRow(20);
824 + }
825 });
826 expect(Scheduler).toHaveYielded(['Up', 'Down']);
827 expect(root).toMatchRenderedOutput(<span prop="Down" />);
@@ -955,11 +961,15 @@ describe('ReactHooksWithNoopRenderer', () => {
961 ReactNoop.flushSync(() => {
962 counter.current.dispatch(INCREMENT);
963 });
958 - expect(Scheduler).toHaveYielded(['Count: 1']);
959 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
960 -
961 - expect(Scheduler).toFlushAndYield(['Count: 4']);
962 - expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
964 + if (gate(flags => flags.enableUnifiedSyncLane)) {
965 + expect(Scheduler).toHaveYielded(['Count: 4']);
966 + expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
967 + } else {
968 + expect(Scheduler).toHaveYielded(['Count: 1']);
969 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
970 + expect(Scheduler).toFlushAndYield(['Count: 4']);
971 + expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
972 + }
973 });
974 });
975
@@ -1717,11 +1727,15 @@ describe('ReactHooksWithNoopRenderer', () => {
1727 // As a result we, somewhat surprisingly, commit them in the opposite order.
1728 // This should be fine because any non-discrete set of work doesn't guarantee order
1729 // and easily could've happened slightly later too.
1720 - expect(Scheduler).toHaveYielded([
1721 - 'Will set count to 1',
1722 - 'Count: 2',
1723 - 'Count: 1',
1724 - ]);
1730 + if (gate(flags => flags.enableUnifiedSyncLane)) {
1731 + expect(Scheduler).toHaveYielded(['Will set count to 1', 'Count: 1']);
1732 + } else {
1733 + expect(Scheduler).toHaveYielded([
1734 + 'Will set count to 1',
1735 + 'Count: 2',
1736 + 'Count: 1',
1737 + ]);
1738 + }
1739
1740 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1741 });
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+138 -102
@@ -47,7 +47,7 @@ describe('ReactIncrementalUpdates', () => {
47 state = {};
48 componentDidMount() {
49 Scheduler.unstable_yieldValue('commit');
50 - ReactNoop.deferredUpdates(() => {
50 + React.startTransition(() => {
51 // Has low priority
52 this.setState({b: 'b'});
53 this.setState({c: 'c'});
@@ -111,13 +111,13 @@ describe('ReactIncrementalUpdates', () => {
111 expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']);
112
113 ReactNoop.flushSync(() => {
114 - ReactNoop.deferredUpdates(() => {
114 + React.startTransition(() => {
115 instance.setState({x: 'x'});
116 instance.setState({y: 'y'});
117 });
118 instance.setState({a: 'a'});
119 instance.setState({b: 'b'});
120 - ReactNoop.deferredUpdates(() => {
120 + React.startTransition(() => {
121 instance.updater.enqueueReplaceState(instance, {c: 'c'});
122 instance.setState({d: 'd'});
123 });
@@ -162,7 +162,11 @@ describe('ReactIncrementalUpdates', () => {
162 }
163
164 // Schedule some async updates
165 - if (gate(flags => flags.enableSyncDefaultUpdates)) {
165 + if (
166 + gate(
167 + flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
168 + )
169 + ) {
170 React.startTransition(() => {
171 instance.setState(createUpdate('a'));
172 instance.setState(createUpdate('b'));
@@ -179,23 +183,37 @@ describe('ReactIncrementalUpdates', () => {
183 expect(ReactNoop.getChildren()).toEqual([span('')]);
184
185 // Schedule some more updates at different priorities
182 - if (gate(flags => flags.enableSyncDefaultUpdates)) {
183 - instance.setState(createUpdate('d'));
184 - ReactNoop.flushSync(() => {
185 - instance.setState(createUpdate('e'));
186 - instance.setState(createUpdate('f'));
187 - });
188 - React.startTransition(() => {
189 - instance.setState(createUpdate('g'));
190 - });
186 + instance.setState(createUpdate('d'));
187 + ReactNoop.flushSync(() => {
188 + instance.setState(createUpdate('e'));
189 + instance.setState(createUpdate('f'));
190 + });
191 + React.startTransition(() => {
192 + instance.setState(createUpdate('g'));
193 + });
194
192 - // The sync updates should have flushed, but not the async ones
195 + // The sync updates should have flushed, but not the async ones.
196 + if (
197 + gate(
198 + flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
199 + )
200 + ) {
201 + expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
202 + expect(ReactNoop.getChildren()).toEqual([span('def')]);
203 + } else {
204 + // Update d was dropped and replaced by e.
205 expect(Scheduler).toHaveYielded(['e', 'f']);
206 expect(ReactNoop.getChildren()).toEqual([span('ef')]);
207 + }
208
196 - // Now flush the remaining work. Even though e and f were already processed,
197 - // they should be processed again, to ensure that the terminal state
198 - // is deterministic.
209 + // Now flush the remaining work. Even though e and f were already processed,
210 + // they should be processed again, to ensure that the terminal state
211 + // is deterministic.
212 + if (
213 + gate(
214 + flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
215 + )
216 + ) {
217 expect(Scheduler).toFlushAndYield([
218 // Since 'g' is in a transition, we'll process 'd' separately first.
219 // That causes us to process 'd' with 'e' and 'f' rebased.
@@ -211,25 +229,19 @@ describe('ReactIncrementalUpdates', () => {
229 'f',
230 'g',
231 ]);
214 - expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
232 } else {
216 - instance.setState(createUpdate('d'));
217 - ReactNoop.flushSync(() => {
218 - instance.setState(createUpdate('e'));
219 - instance.setState(createUpdate('f'));
220 - });
221 - instance.setState(createUpdate('g'));
222 -
223 - // The sync updates should have flushed, but not the async ones
224 - expect(Scheduler).toHaveYielded(['e', 'f']);
225 - expect(ReactNoop.getChildren()).toEqual([span('ef')]);
226 -
227 - // Now flush the remaining work. Even though e and f were already processed,
228 - // they should be processed again, to ensure that the terminal state
229 - // is deterministic.
230 - expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
231 - expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
233 + expect(Scheduler).toFlushAndYield([
234 + // Then we'll re-process everything for 'g'.
235 + 'a',
236 + 'b',
237 + 'c',
238 + 'd',
239 + 'e',
240 + 'f',
241 + 'g',
242 + ]);
243 }
244 + expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
245 });
246
247 it('can abort an update, schedule a replaceState, and resume', () => {
@@ -261,7 +273,11 @@ describe('ReactIncrementalUpdates', () => {
273 }
274
275 // Schedule some async updates
264 - if (gate(flags => flags.enableSyncDefaultUpdates)) {
276 + if (
277 + gate(
278 + flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
279 + )
280 + ) {
281 React.startTransition(() => {
282 instance.setState(createUpdate('a'));
283 instance.setState(createUpdate('b'));
@@ -278,26 +294,39 @@ describe('ReactIncrementalUpdates', () => {
294 expect(ReactNoop.getChildren()).toEqual([span('')]);
295
296 // Schedule some more updates at different priorities
281 - if (gate(flags => flags.enableSyncDefaultUpdates)) {
282 - instance.setState(createUpdate('d'));
297 + instance.setState(createUpdate('d'));
298
284 - ReactNoop.flushSync(() => {
285 - instance.setState(createUpdate('e'));
286 - // No longer a public API, but we can test that it works internally by
287 - // reaching into the updater.
288 - instance.updater.enqueueReplaceState(instance, createUpdate('f'));
289 - });
290 - React.startTransition(() => {
291 - instance.setState(createUpdate('g'));
292 - });
299 + ReactNoop.flushSync(() => {
300 + instance.setState(createUpdate('e'));
301 + // No longer a public API, but we can test that it works internally by
302 + // reaching into the updater.
303 + instance.updater.enqueueReplaceState(instance, createUpdate('f'));
304 + });
305 + React.startTransition(() => {
306 + instance.setState(createUpdate('g'));
307 + });
308
294 - // The sync updates should have flushed, but not the async ones.
309 + // The sync updates should have flushed, but not the async ones.
310 + if (
311 + gate(
312 + flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
313 + )
314 + ) {
315 + expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
316 + } else {
317 + // Update d was dropped and replaced by e.
318 expect(Scheduler).toHaveYielded(['e', 'f']);
296 - expect(ReactNoop.getChildren()).toEqual([span('f')]);
297 -
298 - // Now flush the remaining work. Even though e and f were already processed,
299 - // they should be processed again, to ensure that the terminal state
300 - // is deterministic.
319 + }
320 + expect(ReactNoop.getChildren()).toEqual([span('f')]);
321 +
322 + // Now flush the remaining work. Even though e and f were already processed,
323 + // they should be processed again, to ensure that the terminal state
324 + // is deterministic.
325 + if (
326 + gate(
327 + flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
328 + )
329 + ) {
330 expect(Scheduler).toFlushAndYield([
331 // Since 'g' is in a transition, we'll process 'd' separately first.
332 // That causes us to process 'd' with 'e' and 'f' rebased.
@@ -313,28 +342,19 @@ describe('ReactIncrementalUpdates', () => {
342 'f',
343 'g',
344 ]);
316 - expect(ReactNoop.getChildren()).toEqual([span('fg')]);
345 } else {
318 - instance.setState(createUpdate('d'));
319 - ReactNoop.flushSync(() => {
320 - instance.setState(createUpdate('e'));
321 - // No longer a public API, but we can test that it works internally by
322 - // reaching into the updater.
323 - instance.updater.enqueueReplaceState(instance, createUpdate('f'));
324 - });
325 - instance.setState(createUpdate('g'));
326 -
327 - // The sync updates should have flushed, but not the async ones. Update d
328 - // was dropped and replaced by e.
329 - expect(Scheduler).toHaveYielded(['e', 'f']);
330 - expect(ReactNoop.getChildren()).toEqual([span('f')]);
331 -
332 - // Now flush the remaining work. Even though e and f were already processed,
333 - // they should be processed again, to ensure that the terminal state
334 - // is deterministic.
335 - expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
336 - expect(ReactNoop.getChildren()).toEqual([span('fg')]);
346 + expect(Scheduler).toFlushAndYield([
347 + // Then we'll re-process everything for 'g'.
348 + 'a',
349 + 'b',
350 + 'c',
351 + 'd',
352 + 'e',
353 + 'f',
354 + 'g',
355 + ]);
356 }
357 + expect(ReactNoop.getChildren()).toEqual([span('fg')]);
358 });
359
360 it('passes accumulation of previous updates to replaceState updater function', () => {
@@ -688,21 +708,29 @@ describe('ReactIncrementalUpdates', () => {
708 pushToLog('B'),
709 );
710 });
691 - expect(Scheduler).toHaveYielded([
692 - // A and B are pending. B is higher priority, so we'll render that first.
693 - 'Committed: B',
694 - // Because A comes first in the queue, we're now in rebase mode. B must
695 - // be rebased on top of A. Also, in a layout effect, we received two new
696 - // updates: C and D. C is user-blocking and D is synchronous.
697 - //
698 - // First render the synchronous update. What we're testing here is that
699 - // B *is not dropped* even though it has lower than sync priority. That's
700 - // because we already committed it. However, this render should not
701 - // include C, because that update wasn't already committed.
702 - 'Committed: BD',
703 - 'Committed: BCD',
704 - 'Committed: ABCD',
705 - ]);
711 + if (gate(flags => flags.enableUnifiedSyncLane)) {
712 + expect(Scheduler).toHaveYielded([
713 + 'Committed: B',
714 + 'Committed: BCD',
715 + 'Committed: ABCD',
716 + ]);
717 + } else {
718 + expect(Scheduler).toHaveYielded([
719 + // A and B are pending. B is higher priority, so we'll render that first.
720 + 'Committed: B',
721 + // Because A comes first in the queue, we're now in rebase mode. B must
722 + // be rebased on top of A. Also, in a layout effect, we received two new
723 + // updates: C and D. C is user-blocking and D is synchronous.
724 + //
725 + // First render the synchronous update. What we're testing here is that
726 + // B *is not dropped* even though it has lower than sync priority. That's
727 + // because we already committed it. However, this render should not
728 + // include C, because that update wasn't already committed.
729 + 'Committed: BD',
730 + 'Committed: BCD',
731 + 'Committed: ABCD',
732 + ]);
733 + }
734 expect(root).toMatchRenderedOutput('ABCD');
735 });
736
@@ -748,21 +776,29 @@ describe('ReactIncrementalUpdates', () => {
776 pushToLog('B'),
777 );
778 });
751 - expect(Scheduler).toHaveYielded([
752 - // A and B are pending. B is higher priority, so we'll render that first.
753 - 'Committed: B',
754 - // Because A comes first in the queue, we're now in rebase mode. B must
755 - // be rebased on top of A. Also, in a layout effect, we received two new
756 - // updates: C and D. C is user-blocking and D is synchronous.
757 - //
758 - // First render the synchronous update. What we're testing here is that
759 - // B *is not dropped* even though it has lower than sync priority. That's
760 - // because we already committed it. However, this render should not
761 - // include C, because that update wasn't already committed.
762 - 'Committed: BD',
763 - 'Committed: BCD',
764 - 'Committed: ABCD',
765 - ]);
779 + if (gate(flags => flags.enableUnifiedSyncLane)) {
780 + expect(Scheduler).toHaveYielded([
781 + 'Committed: B',
782 + 'Committed: BCD',
783 + 'Committed: ABCD',
784 + ]);
785 + } else {
786 + expect(Scheduler).toHaveYielded([
787 + // A and B are pending. B is higher priority, so we'll render that first.
788 + 'Committed: B',
789 + // Because A comes first in the queue, we're now in rebase mode. B must
790 + // be rebased on top of A. Also, in a layout effect, we received two new
791 + // updates: C and D. C is user-blocking and D is synchronous.
792 + //
793 + // First render the synchronous update. What we're testing here is that
794 + // B *is not dropped* even though it has lower than sync priority. That's
795 + // because we already committed it. However, this render should not
796 + // include C, because that update wasn't already committed.
797 + 'Committed: BD',
798 + 'Committed: BCD',
799 + 'Committed: ABCD',
800 + ]);
801 + }
802 expect(root).toMatchRenderedOutput('ABCD');
803 });
804
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+9 -2
@@ -690,8 +690,15 @@ describe('ReactOffscreen', () => {
690 );
691
692 // Before the inner update can finish, we receive another pair of updates.
693 - setOuter(2);
694 - setInner(2);
693 + if (gate(flags => flags.enableUnifiedSyncLane)) {
694 + React.startTransition(() => {
695 + setOuter(2);
696 + setInner(2);
697 + });
698 + } else {
699 + setOuter(2);
700 + setInner(2);
701 + }
702
703 // Also, before either of these new updates are processed, the hidden
704 // tree is revealed at high priority.
packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
+7 -3
@@ -381,7 +381,9 @@ describe('ReactOffscreen', () => {
381 expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
382
383 await act(async () => {
384 - setStep(1);
384 + React.startTransition(() => {
385 + setStep(1);
386 + });
387 ReactNoop.flushSync(() => {
388 setText('B');
389 });
@@ -513,8 +515,10 @@ describe('ReactOffscreen', () => {
515
516 // Before the tree commits, schedule a concurrent event. The inner update
517 // is to a tree that's just about to be hidden.
516 - setOuter(2);
517 - setInner(2);
518 + startTransition(() => {
519 + setOuter(2);
520 + setInner(2);
521 + });
522
523 // Commit the previous render.
524 jest.runAllTimers();
packages/react-reconciler/src/__tests__/ReactTransition-test.js
+21 -9
@@ -934,16 +934,28 @@ describe('ReactTransition', () => {
934 updateNormalPri();
935 });
936
937 - expect(Scheduler).toHaveYielded([
938 - // Finish transition update.
939 - 'Normal pri: 0',
940 - 'Commit',
937 + if (gate(flags => flags.enableUnifiedSyncLane)) {
938 + expect(Scheduler).toHaveYielded([
939 + 'Normal pri: 0',
940 + 'Commit',
941
942 - // Normal pri update.
943 - 'Transition pri: 1',
944 - 'Normal pri: 1',
945 - 'Commit',
946 - ]);
942 + // Normal pri update.
943 + 'Transition pri: 1',
944 + 'Normal pri: 1',
945 + 'Commit',
946 + ]);
947 + } else {
948 + expect(Scheduler).toHaveYielded([
949 + // Finish transition update.
950 + 'Normal pri: 0',
951 + 'Commit',
952 +
953 + // Normal pri update.
954 + 'Transition pri: 1',
955 + 'Normal pri: 1',
956 + 'Commit',
957 + ]);
958 + }
959
960 expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
961 });
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+9 -2
@@ -1558,8 +1558,15 @@ describe('useMutableSource', () => {
1558 expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
1559 // Mutate in an event. This schedules a subscription update on a, which
1560 // already mounted, but not b, which hasn't subscribed yet.
1561 - mutateA('a1');
1562 - mutateB('b1');
1561 + if (gate(flags => flags.enableUnifiedSyncLane)) {
1562 + React.startTransition(() => {
1563 + mutateA('a1');
1564 + mutateB('b1');
1565 + });
1566 + } else {
1567 + mutateA('a1');
1568 + mutateB('b1');
1569 + }
1570
1571 // Mutate again at lower priority. This will schedule another subscription
1572 // update on a, but not b. When b mounts and subscriptions, the value it
packages/shared/ReactFeatureFlags.js
+2
@@ -151,6 +151,8 @@ export const enableUseRefAccessWarning = false;
151 // Enables time slicing for updates that aren't wrapped in startTransition.
152 export const enableSyncDefaultUpdates = true;
153
154 +export const enableUnifiedSyncLane = __EXPERIMENTAL__;
155 +
156 // Adds an opt-in to time slicing for updates that aren't wrapped in
157 // startTransition. Only relevant when enableSyncDefaultUpdates is disabled.
158 export const allowConcurrentByDefault = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -72,6 +72,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
72 export const enableLazyContextPropagation = false;
73 export const enableLegacyHidden = true;
74 export const enableSyncDefaultUpdates = true;
75 +export const enableUnifiedSyncLane = false;
76 export const allowConcurrentByDefault = true;
77 export const enableCustomElementPropertySupport = false;
78
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
63 export const enableLazyContextPropagation = false;
64 export const enableLegacyHidden = false;
65 export const enableSyncDefaultUpdates = true;
66 +export const enableUnifiedSyncLane = false;
67 export const allowConcurrentByDefault = false;
68 export const enableCustomElementPropertySupport = false;
69
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
63 export const enableLazyContextPropagation = false;
64 export const enableLegacyHidden = false;
65 export const enableSyncDefaultUpdates = true;
66 +export const enableUnifiedSyncLane = __EXPERIMENTAL__;
67 export const allowConcurrentByDefault = false;
68 export const enableCustomElementPropertySupport = false;
69
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -62,6 +62,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
62 export const enableLazyContextPropagation = false;
63 export const enableLegacyHidden = false;
64 export const enableSyncDefaultUpdates = true;
65 +export const enableUnifiedSyncLane = false;
66 export const allowConcurrentByDefault = true;
67
68 export const consoleManagedByDevToolsDuringStrictMode = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
63 export const enableLazyContextPropagation = false;
64 export const enableLegacyHidden = false;
65 export const enableSyncDefaultUpdates = true;
66 +export const enableUnifiedSyncLane = false;
67 export const allowConcurrentByDefault = true;
68 export const enableCustomElementPropertySupport = false;
69
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
63 export const enableLazyContextPropagation = false;
64 export const enableLegacyHidden = false;
65 export const enableSyncDefaultUpdates = true;
66 +export const enableUnifiedSyncLane = __EXPERIMENTAL__;
67 export const allowConcurrentByDefault = false;
68 export const enableCustomElementPropertySupport = false;
69
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
63 export const enableLazyContextPropagation = false;
64 export const enableLegacyHidden = false;
65 export const enableSyncDefaultUpdates = true;
66 +export const enableUnifiedSyncLane = __EXPERIMENTAL__;
67 export const allowConcurrentByDefault = true;
68 export const enableCustomElementPropertySupport = false;
69
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -24,6 +24,7 @@ export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
24 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
25 export const enableLazyContextPropagation = __VARIANT__;
26 export const enableSyncDefaultUpdates = __VARIANT__;
27 +export const enableUnifiedSyncLane = __VARIANT__;
28 export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
29 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
30 export const enableClientRenderFallbackOnTextMismatch = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -31,6 +31,7 @@ export const {
31 disableSchedulerTimeoutInWorkLoop,
32 enableLazyContextPropagation,
33 enableSyncDefaultUpdates,
34 + enableUnifiedSyncLane,
35 enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
36 enableClientRenderFallbackOnTextMismatch,
37 enableTransitionTracing,
packages/use-subscription/src/__tests__/useSubscription-test.js
+7 -1
@@ -454,7 +454,13 @@ describe('useSubscription', () => {
454 observableA.next('a-2');
455
456 // Update again
457 - renderer.update(<Parent observed={observableA} />);
457 + if (gate(flags => flags.enableUnifiedSyncLane)) {
458 + React.startTransition(() => {
459 + renderer.update(<Parent observed={observableA} />);
460 + });
461 + } else {
462 + renderer.update(<Parent observed={observableA} />);
463 + }
464
465 // Flush everything and ensure that the correct subscribable is used
466 expect(Scheduler).toFlushAndYield([