diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
index a94cbe367a8ae..5ed762afb86a1 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
@@ -275,17 +275,32 @@ describe('ReactDOMFiberAsync', () => {
expect(ops).toEqual([]);
});
// Only the active updates have flushed
- expect(container.textContent).toEqual('BC');
- expect(ops).toEqual(['BC']);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(container.textContent).toEqual('ABC');
+ expect(ops).toEqual(['ABC']);
+ } else {
+ expect(container.textContent).toEqual('BC');
+ expect(ops).toEqual(['BC']);
+ }
- instance.push('D');
- expect(container.textContent).toEqual('BC');
- expect(ops).toEqual(['BC']);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ instance.push('D');
+ expect(container.textContent).toEqual('ABC');
+ expect(ops).toEqual(['ABC']);
+ } else {
+ instance.push('D');
+ expect(container.textContent).toEqual('BC');
+ expect(ops).toEqual(['BC']);
+ }
// Flush the async updates
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('ABCD');
- expect(ops).toEqual(['BC', 'ABCD']);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(ops).toEqual(['ABC', 'ABCD']);
+ } else {
+ expect(ops).toEqual(['BC', 'ABCD']);
+ }
});
// @gate www
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index f2bec4ec6a689..6da64f9cda992 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -449,10 +449,9 @@ describe('ReactDOMServerPartialHydration', () => {
expect(deleted.length).toBe(0);
// Performing an update should force it to delete the boundary
- root.render();
-
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render();
+ });
expect(hydrated.length).toBe(1);
expect(deleted.length).toBe(1);
@@ -945,13 +944,12 @@ describe('ReactDOMServerPartialHydration', () => {
root.render();
// At the same time, resolving the promise so that rendering can complete.
- suspend = false;
- resolve();
- await promise;
-
// This should first complete the hydration and then flush the update onto the hydrated state.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
// The new span should be the same since we should have successfully hydrated
// before changing it.
@@ -1093,9 +1091,9 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
- root.render();
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render();
+ });
// Flushing now should delete the existing content and show the fallback.
@@ -1104,12 +1102,11 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
- suspend = false;
- resolve();
- await promise;
-
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
const span = container.getElementsByTagName('span')[0];
expect(span.textContent).toBe('Hi');
@@ -1174,23 +1171,21 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
// Render an update, but leave it still suspended.
- root.render();
-
// Flushing now should delete the existing content and show the fallback.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render();
+ });
expect(container.getElementsByTagName('span').length).toBe(1);
expect(ref.current).toBe(span);
expect(container.textContent).toBe('');
// Unsuspending shows the content.
- suspend = false;
- resolve();
- await promise;
-
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
expect(span.textContent).toBe('Hi');
expect(span.className).toBe('hi');
@@ -1252,20 +1247,21 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
- root.render();
-
// Flushing now should delete the existing content and show the fallback.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render();
+ });
expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
- suspend = false;
- resolve();
- await promise;
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
Scheduler.unstable_flushAll();
jest.runAllTimers();
@@ -1490,13 +1486,12 @@ describe('ReactDOMServerPartialHydration', () => {
);
// At the same time, resolving the promise so that rendering can complete.
- suspend = false;
- resolve();
- await promise;
-
// This should first complete the hydration and then flush the update onto the hydrated state.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
// Since this should have been hydrated, this should still be the same span.
const newSpan = container.getElementsByTagName('span')[0];
@@ -1569,27 +1564,25 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
- root.render(
-
-
- ,
- );
-
// Flushing now should delete the existing content and show the fallback.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
- suspend = false;
- resolve();
- await promise;
-
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
const span = container.getElementsByTagName('span')[0];
expect(span.textContent).toBe('Hi');
@@ -2320,16 +2313,15 @@ describe('ReactDOMServerPartialHydration', () => {
// Render an update, which will be higher or the same priority as pinging the hydration.
// The new update doesn't suspend.
- root.render(
-
-
- ,
- );
-
// Since we're still suspended on the original data, we can't hydrate.
// This will force all expiration times to flush.
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
// This will now be a new span because we weren't able to hydrate before
const newSpan = container.getElementsByTagName('span')[0];
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
index c6da651a813aa..ca795e98d1410 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
@@ -1786,7 +1786,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
- it('can force hydration in response to sync update', () => {
+ it('can force hydration in response to sync update', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(`Child ${text}`);
return (spanRef = ref)}>{text};
@@ -1812,15 +1812,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
const root = ReactDOMClient.hydrateRoot(container, );
expect(Scheduler).toFlushUntilNextPaint(['App A']);
- ReactDOM.flushSync(() => {
- root.render();
+ await act(async () => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
});
expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
// @gate experimental || www
- it('can force hydration in response to continuous update', () => {
+ it('can force hydration in response to continuous update', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(`Child ${text}`);
return (spanRef = ref)}>{text};
@@ -1846,14 +1848,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
const root = ReactDOMClient.hydrateRoot(container, );
expect(Scheduler).toFlushUntilNextPaint(['App A']);
- TODO_scheduleContinuousSchedulerTask(() => {
- root.render();
+ await act(async () => {
+ TODO_scheduleContinuousSchedulerTask(() => {
+ root.render();
+ });
});
- expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
+
+ expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
- it('can force hydration in response to default update', () => {
+ it('can force hydration in response to default update', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(`Child ${text}`);
return (spanRef = ref)}>{text};
@@ -1878,11 +1883,10 @@ describe('ReactDOMServerSelectiveHydration', () => {
const initialSpan = container.getElementsByTagName('span')[0];
const root = ReactDOMClient.hydrateRoot(container, );
expect(Scheduler).toFlushUntilNextPaint(['App A']);
-
- ReactDOM.unstable_batchedUpdates(() => {
+ await act(async () => {
root.render();
});
- expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
+ expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js
index 97a28f88099e0..42b2fc6f9a3d6 100644
--- a/packages/react-reconciler/src/ReactFiberLane.js
+++ b/packages/react-reconciler/src/ReactFiberLane.js
@@ -23,6 +23,7 @@ import {
enableUpdaterTracking,
allowConcurrentByDefault,
enableTransitionTracing,
+ enableUnifiedSyncLane,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
@@ -45,6 +46,8 @@ export const InputContinuousLane: Lane = /* */ 0b0000000000000000000
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
+export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;
+
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
@@ -133,6 +136,12 @@ let nextTransitionLane: Lane = TransitionLane1;
let nextRetryLane: Lane = RetryLane1;
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
+ if (enableUnifiedSyncLane) {
+ const pendingSyncLanes = lanes & SyncUpdateLanes;
+ if (pendingSyncLanes !== 0) {
+ return pendingSyncLanes;
+ }
+ }
switch (getHighestPriorityLane(lanes)) {
case SyncHydrationLane:
return SyncHydrationLane;
@@ -754,46 +763,50 @@ export function getBumpedLaneForHydration(
const renderLane = getHighestPriorityLane(renderLanes);
let lane;
- switch (renderLane) {
- case SyncLane:
- lane = SyncHydrationLane;
- break;
- case InputContinuousLane:
- lane = InputContinuousHydrationLane;
- break;
- case DefaultLane:
- lane = DefaultHydrationLane;
- break;
- case TransitionLane1:
- case TransitionLane2:
- case TransitionLane3:
- case TransitionLane4:
- case TransitionLane5:
- case TransitionLane6:
- case TransitionLane7:
- case TransitionLane8:
- case TransitionLane9:
- case TransitionLane10:
- case TransitionLane11:
- case TransitionLane12:
- case TransitionLane13:
- case TransitionLane14:
- case TransitionLane15:
- case TransitionLane16:
- case RetryLane1:
- case RetryLane2:
- case RetryLane3:
- case RetryLane4:
- lane = TransitionHydrationLane;
- break;
- case IdleLane:
- lane = IdleHydrationLane;
- break;
- default:
- // Everything else is already either a hydration lane, or shouldn't
- // be retried at a hydration lane.
- lane = NoLane;
- break;
+ if (enableUnifiedSyncLane && (renderLane & SyncUpdateLanes) !== NoLane) {
+ lane = SyncHydrationLane;
+ } else {
+ switch (renderLane) {
+ case SyncLane:
+ lane = SyncHydrationLane;
+ break;
+ case InputContinuousLane:
+ lane = InputContinuousHydrationLane;
+ break;
+ case DefaultLane:
+ lane = DefaultHydrationLane;
+ break;
+ case TransitionLane1:
+ case TransitionLane2:
+ case TransitionLane3:
+ case TransitionLane4:
+ case TransitionLane5:
+ case TransitionLane6:
+ case TransitionLane7:
+ case TransitionLane8:
+ case TransitionLane9:
+ case TransitionLane10:
+ case TransitionLane11:
+ case TransitionLane12:
+ case TransitionLane13:
+ case TransitionLane14:
+ case TransitionLane15:
+ case TransitionLane16:
+ case RetryLane1:
+ case RetryLane2:
+ case RetryLane3:
+ case RetryLane4:
+ lane = TransitionHydrationLane;
+ break;
+ case IdleLane:
+ lane = IdleHydrationLane;
+ break;
+ default:
+ // Everything else is already either a hydration lane, or shouldn't
+ // be retried at a hydration lane.
+ lane = NoLane;
+ break;
+ }
}
// Check if the lane we chose is suspended. If so, that indicates that we
diff --git a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
index 7998d2a23177e..f9938a79b53d9 100644
--- a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
@@ -157,12 +157,18 @@ describe('ReactBlockingMode', () => {
}),
);
- // Only the second update should have flushed synchronously
- expect(Scheduler).toHaveYielded(['B1']);
- expect(root).toMatchRenderedOutput('A0B1');
-
// Now flush the first update
- expect(Scheduler).toFlushAndYield(['A1']);
- expect(root).toMatchRenderedOutput('A1B1');
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded(['A1', 'B1']);
+ expect(root).toMatchRenderedOutput('A1B1');
+ } else {
+ // Only the second update should have flushed synchronously
+ expect(Scheduler).toHaveYielded(['B1']);
+ expect(root).toMatchRenderedOutput('A0B1');
+
+ // Now flush the first update
+ expect(Scheduler).toFlushAndYield(['A1']);
+ expect(root).toMatchRenderedOutput('A1B1');
+ }
});
});
diff --git a/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js
index 0f97bb5997186..1342bd7310baa 100644
--- a/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js
@@ -35,9 +35,17 @@ describe('ReactClassSetStateCallback', () => {
expect(Scheduler).toHaveYielded([0]);
await act(async () => {
- app.setState({step: 1}, () =>
- Scheduler.unstable_yieldValue('Callback 1'),
- );
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ React.startTransition(() => {
+ app.setState({step: 1}, () =>
+ Scheduler.unstable_yieldValue('Callback 1'),
+ );
+ });
+ } else {
+ app.setState({step: 1}, () =>
+ Scheduler.unstable_yieldValue('Callback 1'),
+ );
+ }
ReactNoop.flushSync(() => {
app.setState({step: 2}, () =>
Scheduler.unstable_yieldValue('Callback 2'),
diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
index 24127589dfd90..72ca257fefa8d 100644
--- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
@@ -339,7 +339,9 @@ describe('ReactExpiration', () => {
// Before the update can finish, update again. Even though no time has
// advanced, this update should be given a different expiration time than
// the currently rendering one. So, C and D should render with 1, not 2.
- subscribers.forEach(s => s.setState({text: '2'}));
+ React.startTransition(() => {
+ subscribers.forEach(s => s.setState({text: '2'}));
+ });
expect(Scheduler).toFlushAndYieldThrough([
'1 [C] [render]',
'1 [D] [render]',
diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
index 07cc3437a5e24..5802253e1c1f1 100644
--- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
@@ -54,15 +54,21 @@ describe('ReactFlushSync', () => {
// The passive effect will schedule a sync update and a normal update.
// They should commit in two separate batches. First the sync one.
expect(() => {
- expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
+ expect(Scheduler).toFlushUntilNextPaint(
+ gate(flags => flags.enableUnifiedSyncLane) ? ['1, 1'] : ['1, 0'],
+ );
}).toErrorDev('flushSync was called from inside a lifecycle method');
// The remaining update is not sync
ReactNoop.flushSync();
expect(Scheduler).toHaveYielded([]);
- // Now flush it.
- expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toFlushUntilNextPaint([]);
+ } else {
+ // Now flush it.
+ expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
+ }
});
expect(root).toMatchRenderedOutput('1, 1');
});
diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
index 63dc3c04d0a37..ebf4a25446929 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
@@ -568,9 +568,13 @@ describe('ReactHooks', () => {
});
};
- // Update at normal priority
- ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
-
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ // Update at transition priority
+ React.startTransition(() => update(n => n * 100));
+ } else {
+ // Update at normal priority
+ ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
+ }
// The new state is eagerly computed.
expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
index 25d19be6b48d3..8bbf49c2cc514 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
@@ -815,7 +815,13 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.discreteUpdates(() => {
setRow(5);
});
- setRow(20);
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.startTransition(() => {
+ setRow(20);
+ });
+ } else {
+ setRow(20);
+ }
});
expect(Scheduler).toHaveYielded(['Up', 'Down']);
expect(root).toMatchRenderedOutput();
@@ -955,11 +961,15 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.flushSync(() => {
counter.current.dispatch(INCREMENT);
});
- expect(Scheduler).toHaveYielded(['Count: 1']);
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
-
- expect(Scheduler).toFlushAndYield(['Count: 4']);
- expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded(['Count: 4']);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
+ } else {
+ expect(Scheduler).toHaveYielded(['Count: 1']);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
+ expect(Scheduler).toFlushAndYield(['Count: 4']);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
+ }
});
});
@@ -1717,11 +1727,15 @@ describe('ReactHooksWithNoopRenderer', () => {
// As a result we, somewhat surprisingly, commit them in the opposite order.
// This should be fine because any non-discrete set of work doesn't guarantee order
// and easily could've happened slightly later too.
- expect(Scheduler).toHaveYielded([
- 'Will set count to 1',
- 'Count: 2',
- 'Count: 1',
- ]);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded(['Will set count to 1', 'Count: 1']);
+ } else {
+ expect(Scheduler).toHaveYielded([
+ 'Will set count to 1',
+ 'Count: 2',
+ 'Count: 1',
+ ]);
+ }
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
});
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
index 4d667b0ab6707..b56fdc85361b2 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
@@ -47,7 +47,7 @@ describe('ReactIncrementalUpdates', () => {
state = {};
componentDidMount() {
Scheduler.unstable_yieldValue('commit');
- ReactNoop.deferredUpdates(() => {
+ React.startTransition(() => {
// Has low priority
this.setState({b: 'b'});
this.setState({c: 'c'});
@@ -111,13 +111,13 @@ describe('ReactIncrementalUpdates', () => {
expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']);
ReactNoop.flushSync(() => {
- ReactNoop.deferredUpdates(() => {
+ React.startTransition(() => {
instance.setState({x: 'x'});
instance.setState({y: 'y'});
});
instance.setState({a: 'a'});
instance.setState({b: 'b'});
- ReactNoop.deferredUpdates(() => {
+ React.startTransition(() => {
instance.updater.enqueueReplaceState(instance, {c: 'c'});
instance.setState({d: 'd'});
});
@@ -162,7 +162,11 @@ describe('ReactIncrementalUpdates', () => {
}
// Schedule some async updates
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
+ )
+ ) {
React.startTransition(() => {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
@@ -179,23 +183,37 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop.getChildren()).toEqual([span('')]);
// Schedule some more updates at different priorities
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
- instance.setState(createUpdate('d'));
- ReactNoop.flushSync(() => {
- instance.setState(createUpdate('e'));
- instance.setState(createUpdate('f'));
- });
- React.startTransition(() => {
- instance.setState(createUpdate('g'));
- });
+ instance.setState(createUpdate('d'));
+ ReactNoop.flushSync(() => {
+ instance.setState(createUpdate('e'));
+ instance.setState(createUpdate('f'));
+ });
+ React.startTransition(() => {
+ instance.setState(createUpdate('g'));
+ });
- // The sync updates should have flushed, but not the async ones
+ // The sync updates should have flushed, but not the async ones.
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
+ )
+ ) {
+ expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
+ expect(ReactNoop.getChildren()).toEqual([span('def')]);
+ } else {
+ // Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']);
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
+ }
- // Now flush the remaining work. Even though e and f were already processed,
- // they should be processed again, to ensure that the terminal state
- // is deterministic.
+ // Now flush the remaining work. Even though e and f were already processed,
+ // they should be processed again, to ensure that the terminal state
+ // is deterministic.
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
+ )
+ ) {
expect(Scheduler).toFlushAndYield([
// Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased.
@@ -211,25 +229,19 @@ describe('ReactIncrementalUpdates', () => {
'f',
'g',
]);
- expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
} else {
- instance.setState(createUpdate('d'));
- ReactNoop.flushSync(() => {
- instance.setState(createUpdate('e'));
- instance.setState(createUpdate('f'));
- });
- instance.setState(createUpdate('g'));
-
- // The sync updates should have flushed, but not the async ones
- expect(Scheduler).toHaveYielded(['e', 'f']);
- expect(ReactNoop.getChildren()).toEqual([span('ef')]);
-
- // Now flush the remaining work. Even though e and f were already processed,
- // they should be processed again, to ensure that the terminal state
- // is deterministic.
- expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
- expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
+ expect(Scheduler).toFlushAndYield([
+ // Then we'll re-process everything for 'g'.
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ 'g',
+ ]);
}
+ expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
});
it('can abort an update, schedule a replaceState, and resume', () => {
@@ -261,7 +273,11 @@ describe('ReactIncrementalUpdates', () => {
}
// Schedule some async updates
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
+ )
+ ) {
React.startTransition(() => {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
@@ -278,26 +294,39 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop.getChildren()).toEqual([span('')]);
// Schedule some more updates at different priorities
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
- instance.setState(createUpdate('d'));
+ instance.setState(createUpdate('d'));
- ReactNoop.flushSync(() => {
- instance.setState(createUpdate('e'));
- // No longer a public API, but we can test that it works internally by
- // reaching into the updater.
- instance.updater.enqueueReplaceState(instance, createUpdate('f'));
- });
- React.startTransition(() => {
- instance.setState(createUpdate('g'));
- });
+ ReactNoop.flushSync(() => {
+ instance.setState(createUpdate('e'));
+ // No longer a public API, but we can test that it works internally by
+ // reaching into the updater.
+ instance.updater.enqueueReplaceState(instance, createUpdate('f'));
+ });
+ React.startTransition(() => {
+ instance.setState(createUpdate('g'));
+ });
- // The sync updates should have flushed, but not the async ones.
+ // The sync updates should have flushed, but not the async ones.
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
+ )
+ ) {
+ expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
+ } else {
+ // Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']);
- expect(ReactNoop.getChildren()).toEqual([span('f')]);
-
- // Now flush the remaining work. Even though e and f were already processed,
- // they should be processed again, to ensure that the terminal state
- // is deterministic.
+ }
+ expect(ReactNoop.getChildren()).toEqual([span('f')]);
+
+ // Now flush the remaining work. Even though e and f were already processed,
+ // they should be processed again, to ensure that the terminal state
+ // is deterministic.
+ if (
+ gate(
+ flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
+ )
+ ) {
expect(Scheduler).toFlushAndYield([
// Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased.
@@ -313,28 +342,19 @@ describe('ReactIncrementalUpdates', () => {
'f',
'g',
]);
- expect(ReactNoop.getChildren()).toEqual([span('fg')]);
} else {
- instance.setState(createUpdate('d'));
- ReactNoop.flushSync(() => {
- instance.setState(createUpdate('e'));
- // No longer a public API, but we can test that it works internally by
- // reaching into the updater.
- instance.updater.enqueueReplaceState(instance, createUpdate('f'));
- });
- instance.setState(createUpdate('g'));
-
- // The sync updates should have flushed, but not the async ones. Update d
- // was dropped and replaced by e.
- expect(Scheduler).toHaveYielded(['e', 'f']);
- expect(ReactNoop.getChildren()).toEqual([span('f')]);
-
- // Now flush the remaining work. Even though e and f were already processed,
- // they should be processed again, to ensure that the terminal state
- // is deterministic.
- expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
- expect(ReactNoop.getChildren()).toEqual([span('fg')]);
+ expect(Scheduler).toFlushAndYield([
+ // Then we'll re-process everything for 'g'.
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ 'g',
+ ]);
}
+ expect(ReactNoop.getChildren()).toEqual([span('fg')]);
});
it('passes accumulation of previous updates to replaceState updater function', () => {
@@ -688,21 +708,29 @@ describe('ReactIncrementalUpdates', () => {
pushToLog('B'),
);
});
- expect(Scheduler).toHaveYielded([
- // A and B are pending. B is higher priority, so we'll render that first.
- 'Committed: B',
- // Because A comes first in the queue, we're now in rebase mode. B must
- // be rebased on top of A. Also, in a layout effect, we received two new
- // updates: C and D. C is user-blocking and D is synchronous.
- //
- // First render the synchronous update. What we're testing here is that
- // B *is not dropped* even though it has lower than sync priority. That's
- // because we already committed it. However, this render should not
- // include C, because that update wasn't already committed.
- 'Committed: BD',
- 'Committed: BCD',
- 'Committed: ABCD',
- ]);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded([
+ 'Committed: B',
+ 'Committed: BCD',
+ 'Committed: ABCD',
+ ]);
+ } else {
+ expect(Scheduler).toHaveYielded([
+ // A and B are pending. B is higher priority, so we'll render that first.
+ 'Committed: B',
+ // Because A comes first in the queue, we're now in rebase mode. B must
+ // be rebased on top of A. Also, in a layout effect, we received two new
+ // updates: C and D. C is user-blocking and D is synchronous.
+ //
+ // First render the synchronous update. What we're testing here is that
+ // B *is not dropped* even though it has lower than sync priority. That's
+ // because we already committed it. However, this render should not
+ // include C, because that update wasn't already committed.
+ 'Committed: BD',
+ 'Committed: BCD',
+ 'Committed: ABCD',
+ ]);
+ }
expect(root).toMatchRenderedOutput('ABCD');
});
@@ -748,21 +776,29 @@ describe('ReactIncrementalUpdates', () => {
pushToLog('B'),
);
});
- expect(Scheduler).toHaveYielded([
- // A and B are pending. B is higher priority, so we'll render that first.
- 'Committed: B',
- // Because A comes first in the queue, we're now in rebase mode. B must
- // be rebased on top of A. Also, in a layout effect, we received two new
- // updates: C and D. C is user-blocking and D is synchronous.
- //
- // First render the synchronous update. What we're testing here is that
- // B *is not dropped* even though it has lower than sync priority. That's
- // because we already committed it. However, this render should not
- // include C, because that update wasn't already committed.
- 'Committed: BD',
- 'Committed: BCD',
- 'Committed: ABCD',
- ]);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded([
+ 'Committed: B',
+ 'Committed: BCD',
+ 'Committed: ABCD',
+ ]);
+ } else {
+ expect(Scheduler).toHaveYielded([
+ // A and B are pending. B is higher priority, so we'll render that first.
+ 'Committed: B',
+ // Because A comes first in the queue, we're now in rebase mode. B must
+ // be rebased on top of A. Also, in a layout effect, we received two new
+ // updates: C and D. C is user-blocking and D is synchronous.
+ //
+ // First render the synchronous update. What we're testing here is that
+ // B *is not dropped* even though it has lower than sync priority. That's
+ // because we already committed it. However, this render should not
+ // include C, because that update wasn't already committed.
+ 'Committed: BD',
+ 'Committed: BCD',
+ 'Committed: ABCD',
+ ]);
+ }
expect(root).toMatchRenderedOutput('ABCD');
});
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
index d723c44a5d308..cffcf617824ae 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
@@ -690,8 +690,15 @@ describe('ReactOffscreen', () => {
);
// Before the inner update can finish, we receive another pair of updates.
- setOuter(2);
- setInner(2);
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ React.startTransition(() => {
+ setOuter(2);
+ setInner(2);
+ });
+ } else {
+ setOuter(2);
+ setInner(2);
+ }
// Also, before either of these new updates are processed, the hidden
// tree is revealed at high priority.
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
index e1a6ca49fd64c..272979fab6444 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
@@ -381,7 +381,9 @@ describe('ReactOffscreen', () => {
expect(root).toMatchRenderedOutput(A0);
await act(async () => {
- setStep(1);
+ React.startTransition(() => {
+ setStep(1);
+ });
ReactNoop.flushSync(() => {
setText('B');
});
@@ -513,8 +515,10 @@ describe('ReactOffscreen', () => {
// Before the tree commits, schedule a concurrent event. The inner update
// is to a tree that's just about to be hidden.
- setOuter(2);
- setInner(2);
+ startTransition(() => {
+ setOuter(2);
+ setInner(2);
+ });
// Commit the previous render.
jest.runAllTimers();
diff --git a/packages/react-reconciler/src/__tests__/ReactTransition-test.js b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
index 560bb527b1b0e..dd0b9e0d0fedc 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransition-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
@@ -934,16 +934,28 @@ describe('ReactTransition', () => {
updateNormalPri();
});
- expect(Scheduler).toHaveYielded([
- // Finish transition update.
- 'Normal pri: 0',
- 'Commit',
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ expect(Scheduler).toHaveYielded([
+ 'Normal pri: 0',
+ 'Commit',
- // Normal pri update.
- 'Transition pri: 1',
- 'Normal pri: 1',
- 'Commit',
- ]);
+ // Normal pri update.
+ 'Transition pri: 1',
+ 'Normal pri: 1',
+ 'Commit',
+ ]);
+ } else {
+ expect(Scheduler).toHaveYielded([
+ // Finish transition update.
+ 'Normal pri: 0',
+ 'Commit',
+
+ // Normal pri update.
+ 'Transition pri: 1',
+ 'Normal pri: 1',
+ 'Commit',
+ ]);
+ }
expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
});
diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
index 6321168d70aa3..c4ca0ae1de995 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
@@ -1558,8 +1558,15 @@ describe('useMutableSource', () => {
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
// Mutate in an event. This schedules a subscription update on a, which
// already mounted, but not b, which hasn't subscribed yet.
- mutateA('a1');
- mutateB('b1');
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ React.startTransition(() => {
+ mutateA('a1');
+ mutateB('b1');
+ });
+ } else {
+ mutateA('a1');
+ mutateB('b1');
+ }
// Mutate again at lower priority. This will schedule another subscription
// update on a, but not b. When b mounts and subscriptions, the value it
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 8fcd13894c33a..24dc67d374692 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -151,6 +151,8 @@ export const enableUseRefAccessWarning = false;
// Enables time slicing for updates that aren't wrapped in startTransition.
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = __EXPERIMENTAL__;
+
// Adds an opt-in to time slicing for updates that aren't wrapped in
// startTransition. Only relevant when enableSyncDefaultUpdates is disabled.
export const allowConcurrentByDefault = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 6d84e76612421..c43847ff08df5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -72,6 +72,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = true;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 9b5c301b7d5ba..401362abc7f90 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = false;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 45561ee4c5fec..96fd0dd2cca18 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = __EXPERIMENTAL__;
export const allowConcurrentByDefault = false;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index dace39942cf2a..bcdcd974509a8 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -62,6 +62,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true;
export const consoleManagedByDevToolsDuringStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 47ed0529927c6..f2dcfc00f9b55 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index bfcce69fe7308..1dda88f6bc176 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = __EXPERIMENTAL__;
export const allowConcurrentByDefault = false;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index e45ef9d1af50c..cd4535e761a10 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -63,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
+export const enableUnifiedSyncLane = __EXPERIMENTAL__;
export const allowConcurrentByDefault = true;
export const enableCustomElementPropertySupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index 882871de6f507..12b4ba8d35fde 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -24,6 +24,7 @@ export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const enableLazyContextPropagation = __VARIANT__;
export const enableSyncDefaultUpdates = __VARIANT__;
+export const enableUnifiedSyncLane = __VARIANT__;
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
export const enableClientRenderFallbackOnTextMismatch = __VARIANT__;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 89fae798d37b2..73a592286cb88 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -31,6 +31,7 @@ export const {
disableSchedulerTimeoutInWorkLoop,
enableLazyContextPropagation,
enableSyncDefaultUpdates,
+ enableUnifiedSyncLane,
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
enableClientRenderFallbackOnTextMismatch,
enableTransitionTracing,
diff --git a/packages/use-subscription/src/__tests__/useSubscription-test.js b/packages/use-subscription/src/__tests__/useSubscription-test.js
index 985312abd1d64..ee89c3dbb9562 100644
--- a/packages/use-subscription/src/__tests__/useSubscription-test.js
+++ b/packages/use-subscription/src/__tests__/useSubscription-test.js
@@ -454,7 +454,13 @@ describe('useSubscription', () => {
observableA.next('a-2');
// Update again
- renderer.update();
+ if (gate(flags => flags.enableUnifiedSyncLane)) {
+ React.startTransition(() => {
+ renderer.update();
+ });
+ } else {
+ renderer.update();
+ }
// Flush everything and ensure that the correct subscribable is used
expect(Scheduler).toFlushAndYield([