Compare commits

...

7 Commits

Author SHA1 Message Date
Tianyu Yao 6ba43afe36 Address feedback 2023-01-05 14:51:53 -08:00
Tianyu Yao 1cba37e4ae Fix failing tests by using act
Co-authored-by: Andrew Clark <hi@andrewclark.io>
2022-12-15 12:25:23 -08:00
Tianyu Yao 16e3268f1f TODO:getBumpedLaneForHydration 2022-12-15 12:25:23 -08:00
Tianyu Yao eb583f0663 Simplify logic 2022-12-15 12:25:23 -08:00
Tianyu Yao ec78a39f65 Add a separate feature flag 2022-12-15 12:25:23 -08:00
Tianyu Yao e5ac893ab8 fix test 2022-12-15 12:25:23 -08:00
Tianyu Yao 128e7c4cf3 Batch default, continuous and sync lane 2022-12-15 12:25:23 -08:00
26 changed files with 413 additions and 266 deletions

View File

@ -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

View File

@ -449,10 +449,9 @@ describe('ReactDOMServerPartialHydration', () => {
expect(deleted.length).toBe(0);
// Performing an update should force it to delete the boundary
root.render(<App value={true} />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
await act(async () => {
root.render(<App value={true} />);
});
expect(hydrated.length).toBe(1);
expect(deleted.length).toBe(1);
@ -945,13 +944,12 @@ describe('ReactDOMServerPartialHydration', () => {
root.render(<App text="Hi" className="hi" />);
// 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(<App text="Hi" className="hi" />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
await act(async () => {
root.render(<App text="Hi" className="hi" />);
});
// 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(<App text="Hi" className="hi" />);
// Flushing now should delete the existing content and show the fallback.
Scheduler.unstable_flushAll();
jest.runAllTimers();
await act(async () => {
root.render(<App text="Hi" className="hi" />);
});
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(<App text="Hi" className="hi" />);
// Flushing now should delete the existing content and show the fallback.
Scheduler.unstable_flushAll();
jest.runAllTimers();
await act(async () => {
root.render(<App text="Hi" className="hi" />);
});
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(
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
<App />
</Context.Provider>,
);
// Flushing now should delete the existing content and show the fallback.
Scheduler.unstable_flushAll();
jest.runAllTimers();
await act(async () => {
root.render(
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
<App />
</Context.Provider>,
);
});
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(
<ClassName.Provider value={'hi'}>
<App text="Hi" />
</ClassName.Provider>,
);
// 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(
<ClassName.Provider value={'hi'}>
<App text="Hi" />
</ClassName.Provider>,
);
});
// This will now be a new span because we weren't able to hydrate before
const newSpan = container.getElementsByTagName('span')[0];

View File

@ -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 <span ref={ref => (spanRef = ref)}>{text}</span>;
@ -1812,15 +1812,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
expect(Scheduler).toFlushUntilNextPaint(['App A']);
ReactDOM.flushSync(() => {
root.render(<App text="B" />);
await act(async () => {
ReactDOM.flushSync(() => {
root.render(<App text="B" />);
});
});
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 <span ref={ref => (spanRef = ref)}>{text}</span>;
@ -1846,14 +1848,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
expect(Scheduler).toFlushUntilNextPaint(['App A']);
TODO_scheduleContinuousSchedulerTask(() => {
root.render(<App text="B" />);
await act(async () => {
TODO_scheduleContinuousSchedulerTask(() => {
root.render(<App text="B" />);
});
});
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 <span ref={ref => (spanRef = ref)}>{text}</span>;
@ -1878,11 +1883,10 @@ describe('ReactDOMServerSelectiveHydration', () => {
const initialSpan = container.getElementsByTagName('span')[0];
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
expect(Scheduler).toFlushUntilNextPaint(['App A']);
ReactDOM.unstable_batchedUpdates(() => {
await act(async () => {
root.render(<App text="B" />);
});
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);
});

View File

@ -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

View File

@ -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');
}
});
});

View File

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

View File

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

View File

@ -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');
});

View File

@ -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)']);

View File

@ -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(<span prop="Down" />);
@ -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')]);
});

View File

@ -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')]);
}
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.
// 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');
});

View File

@ -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.

View File

@ -381,7 +381,9 @@ describe('ReactOffscreen', () => {
expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
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();

View File

@ -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');
});

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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__;

View File

@ -31,6 +31,7 @@ export const {
disableSchedulerTimeoutInWorkLoop,
enableLazyContextPropagation,
enableSyncDefaultUpdates,
enableUnifiedSyncLane,
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
enableClientRenderFallbackOnTextMismatch,
enableTransitionTracing,

View File

@ -454,7 +454,13 @@ describe('useSubscription', () => {
observableA.next('a-2');
// Update again
renderer.update(<Parent observed={observableA} />);
if (gate(flags => flags.enableUnifiedSyncLane)) {
React.startTransition(() => {
renderer.update(<Parent observed={observableA} />);
});
} else {
renderer.update(<Parent observed={observableA} />);
}
// Flush everything and ensure that the correct subscribable is used
expect(Scheduler).toFlushAndYield([