Make time-slicing opt-in (#21072)

* Add enableSyncDefaultUpdates feature flag

* Add enableSyncDefaultUpdates implementation

* Fix tests

* Switch feature flag to true by default

* Finish concurrent render whenever for non-sync lanes

* Also return DefaultLane with eventLane

* Gate interruption test

* Add continuout native event test

* Fix tests from rebasing main

* Hardcode lanes, remove added export

* Sync forks
This commit is contained in:
Ricky
2021-04-09 19:50:09 -04:00
committed by GitHub
parent b0407b55ff
commit 933880b454
46 changed files with 1994 additions and 536 deletions
@@ -268,6 +268,7 @@ describe('createSubscription', () => {
expect(Scheduler).toFlushAndYield(['b-1']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should ignore values emitted by a new subscribable until the commit phase', () => {
const log = [];
@@ -325,7 +326,13 @@ describe('createSubscription', () => {
expect(log).toEqual(['Parent.componentDidMount']);
// Start React update, but don't finish
ReactNoop.render(<Parent observed={observableB} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent observed={observableB} />);
});
} else {
ReactNoop.render(<Parent observed={observableB} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
@@ -355,6 +362,7 @@ describe('createSubscription', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should not drop values emitted between updates', () => {
const log = [];
@@ -412,7 +420,13 @@ describe('createSubscription', () => {
expect(log).toEqual(['Parent.componentDidMount']);
// Start React update, but don't finish
ReactNoop.render(<Parent observed={observableB} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent observed={observableB} />);
});
} else {
ReactNoop.render(<Parent observed={observableB} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
+1
View File
@@ -360,6 +360,7 @@ describe('ReactART', () => {
expect(onClick2).toBeCalled();
});
// @gate !enableSyncDefaultUpdates
it('can concurrently render with a "primary" renderer while sharing context', () => {
const CurrentRendererContext = React.createContext(null);
@@ -284,6 +284,46 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
expect(container.textContent).toEqual('hovered');
});
// @gate experimental
it('continuous native events flush as expected', async () => {
const root = ReactDOM.unstable_createRoot(container);
const target = React.createRef(null);
function Foo({hovered}) {
const hoverString = hovered ? 'hovered' : 'not hovered';
Scheduler.unstable_yieldValue(hoverString);
return <div ref={target}>{hoverString}</div>;
}
await act(async () => {
root.render(<Foo hovered={false} />);
});
expect(container.textContent).toEqual('not hovered');
await act(async () => {
// Note: React does not use native mouseenter/mouseleave events
// but we should still correctly determine their priority.
const mouseEnterEvent = document.createEvent('MouseEvents');
mouseEnterEvent.initEvent('mouseover', true, true);
target.current.addEventListener('mouseover', () => {
root.render(<Foo hovered={true} />);
});
dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
// Since mouse end is not discrete, should not have updated yet
expect(Scheduler).toHaveYielded(['not hovered']);
expect(container.textContent).toEqual('not hovered');
expect(Scheduler).toFlushAndYieldThrough(['hovered']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(container.textContent).toEqual('hovered');
} else {
expect(container.textContent).toEqual('not hovered');
}
});
expect(container.textContent).toEqual('hovered');
});
// @gate experimental
it('should batch inside native events', async () => {
const root = ReactDOM.unstable_createRoot(container);
@@ -1865,11 +1865,21 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
await act(async () => {
root.render(<App />);
expect(Scheduler).toFlushAndYieldThrough(['Before']);
// This took a long time to render.
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYield(['After']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App />);
});
expect(Scheduler).toFlushAndYieldThrough(['Before', 'After']);
} else {
root.render(<App />);
expect(Scheduler).toFlushAndYieldThrough(['Before']);
// This took a long time to render.
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYield(['After']);
}
// This will cause us to skip the second row completely.
});
@@ -1934,7 +1934,13 @@ describe('DOMPluginEventSystem', () => {
log.length = 0;
// Increase counter
root.render(<Test counter={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Test counter={1} />);
});
} else {
root.render(<Test counter={1} />);
}
// Yield before committing
expect(Scheduler).toFlushAndYieldThrough(['Test']);
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
@@ -32,6 +32,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableStrictEffects,
skipUnmountedBoundaries,
enableSyncDefaultUpdates,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
NoLanes,
NoLane,
SyncLane,
DefaultLane,
DefaultHydrationLane,
InputContinuousLane,
InputContinuousHydrationLane,
NoTimestamp,
claimNextTransitionLane,
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
if (
enableSyncDefaultUpdates &&
(updateLane === InputContinuousLane ||
updateLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return updateLane;
}
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
if (
enableSyncDefaultUpdates &&
(eventLane === InputContinuousLane ||
eventLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return eventLane;
}
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
if (
enableSyncDefaultUpdates &&
(newCallbackPriority === DefaultLane ||
newCallbackPriority === DefaultHydrationLane)
) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
finishConcurrentRender(root, exitStatus, lanes);
} else {
commitRoot(root);
}
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
@@ -32,6 +32,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableStrictEffects,
skipUnmountedBoundaries,
enableSyncDefaultUpdates,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
NoLanes,
NoLane,
SyncLane,
DefaultLane,
DefaultHydrationLane,
InputContinuousLane,
InputContinuousHydrationLane,
NoTimestamp,
claimNextTransitionLane,
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
if (
enableSyncDefaultUpdates &&
(updateLane === InputContinuousLane ||
updateLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return updateLane;
}
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
if (
enableSyncDefaultUpdates &&
(eventLane === InputContinuousLane ||
eventLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return eventLane;
}
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
if (
enableSyncDefaultUpdates &&
(newCallbackPriority === DefaultLane ||
newCallbackPriority === DefaultHydrationLane)
) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
finishConcurrentRender(root, exitStatus, lanes);
} else {
commitRoot(root);
}
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
@@ -45,6 +45,7 @@ describe('ReactSuspenseList', () => {
return Component;
}
// @gate experimental || !enableSyncDefaultUpdates
it('appends rendering tasks to the end of the priority queue', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -63,7 +64,13 @@ describe('ReactSuspenseList', () => {
root.render(<App show={false} />);
expect(Scheduler).toFlushAndYield([]);
root.render(<App show={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App show={true} />);
});
} else {
root.render(<App show={true} />);
}
expect(Scheduler).toFlushAndYield([
'Suspend! [A]',
'Suspend! [B]',
@@ -111,9 +111,15 @@ describe('ReactExpiration', () => {
// Flush the sync task.
ReactNoop.flushSync();
}
// @gate experimental || !enableSyncDefaultUpdates
it('increases priority of updates as time progresses', () => {
ReactNoop.render(<span prop="done" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<span prop="done" />);
});
} else {
ReactNoop.render(<span prop="done" />);
}
expect(ReactNoop.getChildren()).toEqual([]);
@@ -132,6 +138,7 @@ describe('ReactExpiration', () => {
expect(ReactNoop.getChildren()).toEqual([span('done')]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('two updates of like priority in the same event always flush within the same batch', () => {
class TextClass extends React.Component {
componentDidMount() {
@@ -154,7 +161,13 @@ describe('ReactExpiration', () => {
// First, show what happens for updates in two separate events.
// Schedule an update.
ReactNoop.render(<TextClass text="A" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />);
}
// Advance the timer.
Scheduler.unstable_advanceTime(2000);
// Partially flush the first update, then interrupt it.
@@ -184,6 +197,7 @@ describe('ReactExpiration', () => {
expect(Scheduler).toFlushAndYield(['B [render]', 'B [commit]']);
});
// @gate experimental || !enableSyncDefaultUpdates
it(
'two updates of like priority in the same event always flush within the ' +
"same batch, even if there's a sync update in between",
@@ -209,7 +223,13 @@ describe('ReactExpiration', () => {
// First, show what happens for updates in two separate events.
// Schedule an update.
ReactNoop.render(<TextClass text="A" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />);
}
// Advance the timer.
Scheduler.unstable_advanceTime(2000);
// Partially flush the first update, then interrupt it.
@@ -245,6 +265,7 @@ describe('ReactExpiration', () => {
},
);
// @gate experimental || !enableSyncDefaultUpdates
it('cannot update at the same expiration time that is already rendering', () => {
const store = {text: 'initial'};
const subscribers = [];
@@ -281,7 +302,13 @@ describe('ReactExpiration', () => {
}
// Initial mount
ReactNoop.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
}
expect(Scheduler).toFlushAndYield([
'initial [A] [render]',
'initial [B] [render]',
@@ -294,7 +321,13 @@ describe('ReactExpiration', () => {
]);
// Partial update
subscribers.forEach(s => s.setState({text: '1'}));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
subscribers.forEach(s => s.setState({text: '1'}));
});
} else {
subscribers.forEach(s => s.setState({text: '1'}));
}
expect(Scheduler).toFlushAndYieldThrough([
'1 [A] [render]',
'1 [B] [render]',
@@ -310,6 +343,7 @@ describe('ReactExpiration', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('stops yielding if CPU-bound update takes too long to finish', () => {
const root = ReactNoop.createRoot();
function App() {
@@ -324,7 +358,13 @@ describe('ReactExpiration', () => {
);
}
root.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
}
expect(Scheduler).toFlushAndYieldThrough(['A']);
expect(Scheduler).toFlushAndYieldThrough(['B']);
@@ -337,6 +377,7 @@ describe('ReactExpiration', () => {
expect(root).toMatchRenderedOutput('ABCDE');
});
// @gate experimental || !enableSyncDefaultUpdates
it('root expiration is measured from the time of the first update', () => {
Scheduler.unstable_advanceTime(10000);
@@ -352,8 +393,13 @@ describe('ReactExpiration', () => {
</>
);
}
root.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
}
expect(Scheduler).toFlushAndYieldThrough(['A']);
expect(Scheduler).toFlushAndYieldThrough(['B']);
@@ -366,6 +412,7 @@ describe('ReactExpiration', () => {
expect(root).toMatchRenderedOutput('ABCDE');
});
// @gate experimental || !enableSyncDefaultUpdates
it('should measure expiration times relative to module initialization', () => {
// Tests an implementation detail where expiration times are computed using
// bitwise operations.
@@ -381,12 +428,24 @@ describe('ReactExpiration', () => {
// current time.
ReactNoop = require('react-noop-renderer');
ReactNoop.render('Hi');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render('Hi');
});
} else {
ReactNoop.render('Hi');
}
// The update should not have expired yet.
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
expect(ReactNoop).toMatchRenderedOutput(null);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// TODO: Why is this flushed?
expect(ReactNoop).toMatchRenderedOutput('Hi');
} else {
expect(ReactNoop).toMatchRenderedOutput(null);
}
// Advance the time some more to expire the update.
Scheduler.unstable_advanceTime(10000);
@@ -395,6 +454,7 @@ describe('ReactExpiration', () => {
expect(ReactNoop).toMatchRenderedOutput('Hi');
});
// @gate experimental || !enableSyncDefaultUpdates
it('should measure callback timeout relative to current time, not start-up time', () => {
// Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
// The bug wasn't caught by other tests because we use virtual times that
@@ -403,7 +463,13 @@ describe('ReactExpiration', () => {
// Before scheduling an update, advance the current time.
Scheduler.unstable_advanceTime(10000);
ReactNoop.render('Hi');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render('Hi');
});
} else {
ReactNoop.render('Hi');
}
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -418,6 +484,7 @@ describe('ReactExpiration', () => {
expect(ReactNoop).toMatchRenderedOutput('Hi');
});
// @gate experimental || !enableSyncDefaultUpdates
it('prevents starvation by sync updates', async () => {
const {useState} = React;
@@ -450,7 +517,13 @@ describe('ReactExpiration', () => {
// First demonstrate what happens when there's no starvation
await ReactNoop.act(async () => {
updateNormalPri();
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri();
}
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 0']);
updateSyncPri();
});
@@ -466,7 +539,13 @@ describe('ReactExpiration', () => {
// Do the same thing, but starve the first update
await ReactNoop.act(async () => {
updateNormalPri();
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri();
}
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 1']);
// This time, a lot of time has elapsed since the normal pri update
@@ -628,6 +707,7 @@ describe('ReactExpiration', () => {
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('detects starvation in multiple batches', async () => {
const {useState} = React;
@@ -666,7 +746,13 @@ describe('ReactExpiration', () => {
await ReactNoop.act(async () => {
// Partially render an update
updateNormalPri();
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri();
}
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
// Some time goes by. In an interleaved event, schedule another update.
// This will be placed into a separate batch.
@@ -693,6 +779,7 @@ describe('ReactExpiration', () => {
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('updates do not expire while they are IO-bound', async () => {
const {Suspense} = React;
@@ -715,7 +802,13 @@ describe('ReactExpiration', () => {
expect(root).toMatchRenderedOutput('A, Sibling');
await ReactNoop.act(async () => {
root.render(<App text="B" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App text="B" />);
});
} else {
root.render(<App text="B" />);
}
expect(Scheduler).toFlushAndYield([
'Suspend! [B]',
'Sibling',
@@ -22,6 +22,7 @@ describe('ReactFlushSync', () => {
return text;
}
// @gate experimental || !enableSyncDefaultUpdates
test('changes priority of updates in useEffect', async () => {
function App() {
const [syncState, setSyncState] = useState(0);
@@ -37,22 +38,37 @@ describe('ReactFlushSync', () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
}
// This will yield right before the passive effect fires
expect(Scheduler).toFlushUntilNextPaint(['0, 0']);
// 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']),
).toErrorDev('flushSync was called from inside a lifecycle method');
expect(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(Scheduler).toFlushUntilNextPaint(['1, 0', '1, 1']);
} else {
expect(Scheduler).toFlushUntilNextPaint(['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.enableSyncDefaultUpdates)) {
// With sync default updates, passive effects are synchronously flushed.
expect(Scheduler).toHaveYielded([]);
} else {
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
}
});
expect(root).toMatchRenderedOutput('1, 1');
});
@@ -151,6 +151,7 @@ describe('ReactHooksWithNoopRenderer', () => {
return Promise.resolve().then(() => {});
}
// @gate experimental || !enableSyncDefaultUpdates
it('resumes after an interruption', () => {
function Counter(props, ref) {
const [count, updateCount] = useState(0);
@@ -166,10 +167,20 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
// Schedule some updates
ReactNoop.batchedUpdates(() => {
counter.current.updateCount(1);
counter.current.updateCount(count => count + 10);
});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
// TODO: Batched updates need to be inside startTransition?
ReactNoop.batchedUpdates(() => {
counter.current.updateCount(1);
counter.current.updateCount(count => count + 10);
});
});
} else {
ReactNoop.batchedUpdates(() => {
counter.current.updateCount(1);
counter.current.updateCount(count => count + 10);
});
}
// Partially flush without committing
expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
@@ -690,6 +701,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span(22)]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('discards render phase updates if something suspends', async () => {
const thenable = {then() {}};
function Foo({signal}) {
@@ -725,15 +737,28 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield([0]);
expect(root).toMatchRenderedOutput(<span prop={0} />);
root.render(<Foo signal={false} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
}
expect(Scheduler).toFlushAndYield(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop={0} />);
// Rendering again should suspend again.
root.render(<Foo signal={false} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
}
expect(Scheduler).toFlushAndYield(['Suspend!']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
const thenable = {then() {}};
function Foo({signal}) {
@@ -776,19 +801,38 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
await ReactNoop.act(async () => {
root.render(<Foo signal={false} />);
setLabel('B');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Foo signal={false} />);
setLabel('B');
});
} else {
root.render(<Foo signal={false} />);
setLabel('B');
}
expect(Scheduler).toFlushAndYield(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
// Rendering again should suspend again.
root.render(<Foo signal={false} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
}
expect(Scheduler).toFlushAndYield(['Suspend!']);
// Flip the signal back to "cancel" the update. However, the update to
// label should still proceed. It shouldn't have been dropped.
root.render(<Foo signal={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Foo signal={true} />);
});
} else {
root.render(<Foo signal={true} />);
}
expect(Scheduler).toFlushAndYield(['B:0']);
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
});
@@ -1284,6 +1328,7 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => {
let setParentState = null;
const setChildStates = [];
@@ -1350,17 +1395,34 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
// Schedule another update for children, and partially process it.
setChildStates.forEach(setChildState => setChildState(2));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
setChildStates.forEach(setChildState => setChildState(2));
});
} else {
setChildStates.forEach(setChildState => setChildState(2));
}
expect(Scheduler).toFlushAndYieldThrough(['Child one render']);
// Schedule unmount for the parent that unmounts children with pending update.
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
setParentState(false);
});
expect(Scheduler).toFlushUntilNextPaint([
'Parent false render',
'Parent false commit',
]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(Scheduler).toFlushUntilNextPaint([
// TODO: why do the children render and fire effects?
'Child two render',
'Child one commit',
'Child two commit',
'Parent false render',
'Parent false commit',
]);
} else {
expect(Scheduler).toFlushUntilNextPaint([
'Parent false render',
'Parent false commit',
]);
}
// Schedule updates for children too (which should be ignored)
setChildStates.forEach(setChildState => setChildState(2));
@@ -1647,6 +1709,7 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('updates have async priority even if effects are flushed early', () => {
function Counter(props) {
const [count, updateCount] = useState('(empty)');
@@ -1667,20 +1730,41 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
// Rendering again should flush the previous commit's effects
ReactNoop.render(<Counter count={1} />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Counter count={1} />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
});
} else {
ReactNoop.render(<Counter count={1} />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
}
expect(Scheduler).toFlushAndYieldThrough([
'Schedule update [0]',
'Count: 0',
]);
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
ReactNoop.flushPassiveEffects();
expect(Scheduler).toHaveYielded(['Schedule update [1]']);
expect(Scheduler).toFlushAndYield(['Count: 1']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
expect(Scheduler).toFlushAndYieldThrough([
'Count: 0',
'Sync effect',
'Schedule update [1]',
'Count: 1',
]);
} else {
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
ReactNoop.flushPassiveEffects();
expect(Scheduler).toHaveYielded(['Schedule update [1]']);
expect(Scheduler).toFlushAndYield(['Count: 1']);
}
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
});
});
@@ -50,6 +50,7 @@ describe('ReactIncremental', () => {
expect(Scheduler).toFlushWithoutYielding();
});
// @gate experimental || !enableSyncDefaultUpdates
it('should render a simple component, in steps if needed', () => {
function Bar() {
Scheduler.unstable_yieldValue('Bar');
@@ -65,7 +66,17 @@ describe('ReactIncremental', () => {
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
}
ReactNoop.render(<Foo />, () => Scheduler.unstable_yieldValue('callback'));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />, () =>
Scheduler.unstable_yieldValue('callback'),
);
});
} else {
ReactNoop.render(<Foo />, () =>
Scheduler.unstable_yieldValue('callback'),
);
}
// Do one step of work.
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
@@ -132,6 +143,7 @@ describe('ReactIncremental', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('can cancel partially rendered work and restart', () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
@@ -152,13 +164,26 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo text="foo" />);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
ReactNoop.render(<Foo text="bar" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo text="bar" />);
});
} else {
ReactNoop.render(<Foo text="bar" />);
}
// Flush part of the work
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
// This will abort the previous work and restart
ReactNoop.flushSync(() => ReactNoop.render(null));
ReactNoop.render(<Foo text="baz" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo text="baz" />);
});
} else {
ReactNoop.render(<Foo text="baz" />);
}
// Flush part of the new work
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
@@ -167,6 +192,7 @@ describe('ReactIncremental', () => {
expect(Scheduler).toFlushAndYield(['Bar']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should call callbacks even if updates are aborted', () => {
let inst;
@@ -192,26 +218,50 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState1');
return {text: 'bar'};
},
() => Scheduler.unstable_yieldValue('callback1'),
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState1');
return {text: 'bar'};
},
() => Scheduler.unstable_yieldValue('callback1'),
);
});
} else {
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState1');
return {text: 'bar'};
},
() => Scheduler.unstable_yieldValue('callback1'),
);
}
// Flush part of the work
expect(Scheduler).toFlushAndYieldThrough(['setState1']);
// This will abort the previous work and restart
ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState2');
return {text2: 'baz'};
},
() => Scheduler.unstable_yieldValue('callback2'),
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState2');
return {text2: 'baz'};
},
() => Scheduler.unstable_yieldValue('callback2'),
);
});
} else {
inst.setState(
() => {
Scheduler.unstable_yieldValue('setState2');
return {text2: 'baz'};
},
() => Scheduler.unstable_yieldValue('callback2'),
);
}
// Flush the rest of the work which now includes the low priority
expect(Scheduler).toFlushAndYield([
@@ -1714,6 +1764,7 @@ describe('ReactIncremental', () => {
expect(instance.state.n).toEqual(3);
});
// @gate experimental || !enableSyncDefaultUpdates
it('merges and masks context', () => {
class Intl extends React.Component {
static childContextTypes = {
@@ -1838,15 +1889,27 @@ describe('ReactIncremental', () => {
'ShowLocale {"locale":"de"}',
'ShowBoth {"locale":"de"}',
]);
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
});
} else {
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['Intl {}']);
ReactNoop.render(
@@ -1994,18 +2057,35 @@ describe('ReactIncremental', () => {
}
}
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<LegacyHiddenDiv mode="hidden">
<ShowLocale />
<Intl locale="ru">
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
</Intl>
</LegacyHiddenDiv>
<ShowLocale />
</Intl>,
);
<LegacyHiddenDiv mode="hidden">
<ShowLocale />
<Intl locale="ru">
<ShowLocale />
</Intl>
</LegacyHiddenDiv>
<ShowLocale />
</Intl>,
);
});
} else {
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<LegacyHiddenDiv mode="hidden">
<ShowLocale />
<Intl locale="ru">
<ShowLocale />
</Intl>
</LegacyHiddenDiv>
<ShowLocale />
</Intl>,
);
}
expect(Scheduler).toFlushAndYieldThrough([
'Intl {}',
'ShowLocale {"locale":"fr"}',
@@ -2748,6 +2828,7 @@ describe('ReactIncremental', () => {
expect(Scheduler).toFlushAndYield(['count:1, name:not brian']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('does not interrupt for update at same priority', () => {
function Parent(props) {
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2759,7 +2840,13 @@ describe('ReactIncremental', () => {
return null;
}
ReactNoop.render(<Parent step={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
// Interrupt at same priority
@@ -2768,6 +2855,7 @@ describe('ReactIncremental', () => {
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('does not interrupt for update at lower priority', () => {
function Parent(props) {
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2779,7 +2867,13 @@ describe('ReactIncremental', () => {
return null;
}
ReactNoop.render(<Parent step={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
// Interrupt at lower priority
@@ -2789,6 +2883,7 @@ describe('ReactIncremental', () => {
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('does interrupt for update at higher priority', () => {
function Parent(props) {
Scheduler.unstable_yieldValue('Parent: ' + props.step);
@@ -2800,7 +2895,13 @@ describe('ReactIncremental', () => {
return null;
}
ReactNoop.render(<Parent step={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
// Interrupt at higher priority
@@ -58,6 +58,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
}
// @gate experimental || !enableSyncDefaultUpdates
it('recovers from errors asynchronously', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -90,19 +91,37 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('oops!');
}
ReactNoop.render(
<ErrorBoundary>
<Indirection>
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<ErrorBoundary>
<Indirection>
<Indirection>
<Indirection>
<BadRender />
<Indirection />
<Indirection />
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>,
);
});
} else {
ReactNoop.render(
<ErrorBoundary>
<Indirection>
<Indirection>
<BadRender />
<Indirection />
<Indirection />
<Indirection>
<BadRender />
<Indirection />
<Indirection />
</Indirection>
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>,
);
</ErrorBoundary>,
);
}
// Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([
@@ -152,6 +171,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -184,19 +204,37 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('oops!');
}
ReactNoop.render(
<ErrorBoundary>
<Indirection>
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<ErrorBoundary>
<Indirection>
<Indirection>
<Indirection>
<BadRender />
<Indirection />
<Indirection />
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>,
);
});
} else {
ReactNoop.render(
<ErrorBoundary>
<Indirection>
<Indirection>
<BadRender />
<Indirection />
<Indirection />
<Indirection>
<BadRender />
<Indirection />
<Indirection />
</Indirection>
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>,
);
</ErrorBoundary>,
);
}
// Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([
@@ -247,27 +285,16 @@ describe('ReactIncrementalErrorHandling', () => {
Scheduler.unstable_yieldValue('commit');
}
function interrupt() {
ReactNoop.flushSync(() => {
ReactNoop.renderToRootWithID(null, 'other-root');
});
}
ReactNoop.render(<App isBroken={true} />, onCommit);
React.unstable_startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough(['error']);
interrupt();
React.unstable_startTransition(() => {
// This update is in a separate batch
ReactNoop.render(<App isBroken={false} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough([
// The first render fails. But because there's a lower priority pending
// update, it doesn't throw.
'error',
]);
// React will try to recover by rendering all the pending updates in a
// single batch, synchronously. This time it succeeds.
//
@@ -306,15 +333,10 @@ describe('ReactIncrementalErrorHandling', () => {
Scheduler.unstable_yieldValue('commit');
}
function interrupt() {
ReactNoop.flushSync(() => {
ReactNoop.renderToRootWithID(null, 'other-root');
});
}
ReactNoop.render(<App isBroken={true} />, onCommit);
React.unstable_startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough(['error']);
interrupt();
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -323,12 +345,6 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.render(<App isBroken={false} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough([
// The first render fails. But because there's a lower priority pending
// update, it doesn't throw.
'error',
]);
// React will try to recover by rendering all the pending updates in a
// single batch, synchronously. This time it succeeds.
//
@@ -362,6 +378,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
// @gate experimental || !enableSyncDefaultUpdates
it('retries one more time before handling error', () => {
function BadRender({unused}) {
Scheduler.unstable_yieldValue('BadRender');
@@ -383,7 +400,17 @@ describe('ReactIncrementalErrorHandling', () => {
);
}
ReactNoop.render(<Parent />, () => Scheduler.unstable_yieldValue('commit'));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Parent />, () =>
Scheduler.unstable_yieldValue('commit'),
);
});
} else {
ReactNoop.render(<Parent />, () =>
Scheduler.unstable_yieldValue('commit'),
);
}
// Render the bad component asynchronously
expect(Scheduler).toFlushAndYieldThrough(['Parent', 'BadRender']);
@@ -402,6 +429,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('retries one more time if an error occurs during a render that expires midway through the tree', async () => {
function Oops({unused}) {
Scheduler.unstable_yieldValue('Oops');
@@ -425,7 +453,13 @@ describe('ReactIncrementalErrorHandling', () => {
);
}
ReactNoop.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
}
// Render part of the tree
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
@@ -532,6 +566,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('catches render error in a boundary during partial deferred mounting', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -556,11 +591,21 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello');
}
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
} else {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['ErrorBoundary render success']);
expect(ReactNoop.getChildren()).toEqual([]);
@@ -712,6 +757,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('propagates an error from a noop error boundary during partial deferred mounting', () => {
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
@@ -729,11 +775,21 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello');
}
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
});
} else {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']);
@@ -1805,7 +1861,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
await ReactNoop.act(async () => {
root.render(<Oops />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<Oops />);
});
} else {
root.render(<Oops />);
}
// Render past the component that throws, then yield.
expect(Scheduler).toFlushAndYieldThrough(['Oops']);
@@ -34,6 +34,7 @@ describe('ReactIncrementalReflection', () => {
return {type: 'span', children: [], prop, hidden: false};
}
// @gate experimental || !enableSyncDefaultUpdates
it('handles isMounted even when the initial render is deferred', () => {
const instances = [];
@@ -63,7 +64,13 @@ describe('ReactIncrementalReflection', () => {
return <Component />;
}
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
// Render part way through but don't yet commit the updates.
expect(Scheduler).toFlushAndYieldThrough(['componentWillMount: false']);
@@ -81,6 +88,7 @@ describe('ReactIncrementalReflection', () => {
expect(instances[0]._isMounted()).toBe(true);
});
// @gate experimental || !enableSyncDefaultUpdates
it('handles isMounted when an unmount is deferred', () => {
const instances = [];
@@ -121,7 +129,13 @@ describe('ReactIncrementalReflection', () => {
expect(instances[0]._isMounted()).toBe(true);
ReactNoop.render(<Foo mount={false} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo mount={false} />);
});
} else {
ReactNoop.render(<Foo mount={false} />);
}
// Render part way through but don't yet commit the updates so it is not
// fully unmounted yet.
expect(Scheduler).toFlushAndYieldThrough(['Other']);
@@ -134,6 +148,7 @@ describe('ReactIncrementalReflection', () => {
expect(instances[0]._isMounted()).toBe(false);
});
// @gate experimental || !enableSyncDefaultUpdates
it('finds no node before insertion and correct node before deletion', () => {
let classInstance = null;
@@ -204,7 +219,13 @@ describe('ReactIncrementalReflection', () => {
return [<Component key="a" step={props.step} />, <Sibling key="b" />];
}
ReactNoop.render(<Foo step={0} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={0} />);
});
} else {
ReactNoop.render(<Foo step={0} />);
}
// Flush past Component but don't complete rendering everything yet.
expect(Scheduler).toFlushAndYieldThrough([
['componentWillMount', null],
@@ -246,7 +267,13 @@ describe('ReactIncrementalReflection', () => {
// The next step will render a new host node but won't get committed yet.
// We expect this to mutate the original Fiber.
ReactNoop.render(<Foo step={2} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={2} />);
});
} else {
ReactNoop.render(<Foo step={2} />);
}
expect(Scheduler).toFlushAndYieldThrough([
['componentWillUpdate', hostSpan],
'render',
@@ -267,7 +294,13 @@ describe('ReactIncrementalReflection', () => {
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
// Render to null but don't commit it yet.
ReactNoop.render(<Foo step={3} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={3} />);
});
} else {
ReactNoop.render(<Foo step={3} />);
}
expect(Scheduler).toFlushAndYieldThrough([
['componentWillUpdate', hostDiv],
'render',
@@ -85,6 +85,7 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
});
// @gate experimental || !enableSyncDefaultUpdates
it('works on deferred roots in the order they were scheduled', () => {
const {useEffect} = React;
function Text({text}) {
@@ -107,8 +108,15 @@ describe('ReactIncrementalScheduling', () => {
// Schedule deferred work in the reverse order
ReactNoop.act(() => {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
});
} else {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
}
// Ensure it starts in the order it was scheduled
expect(Scheduler).toFlushAndYieldThrough(['c:2']);
@@ -117,7 +125,13 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
// Schedule last bit of work, it will get processed the last
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
});
} else {
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
}
// Keep performing work in the order it was scheduled
expect(Scheduler).toFlushAndYieldThrough(['b:2']);
@@ -132,6 +146,7 @@ describe('ReactIncrementalScheduling', () => {
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('schedules sync updates when inside componentDidMount/Update', () => {
let instance;
@@ -170,7 +185,13 @@ describe('ReactIncrementalScheduling', () => {
}
}
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
// Render without committing
expect(Scheduler).toFlushAndYieldThrough(['render: 0']);
@@ -184,7 +205,13 @@ describe('ReactIncrementalScheduling', () => {
'componentDidUpdate: 1',
]);
instance.setState({tick: 2});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
instance.setState({tick: 2});
});
} else {
instance.setState({tick: 2});
}
expect(Scheduler).toFlushAndYieldThrough(['render: 2']);
expect(ReactNoop.flushNextYield()).toEqual([
'componentDidUpdate: 2',
@@ -197,6 +224,7 @@ describe('ReactIncrementalScheduling', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('can opt-in to async scheduling inside componentDidMount/Update', () => {
let instance;
class Foo extends React.Component {
@@ -255,20 +283,39 @@ describe('ReactIncrementalScheduling', () => {
// Increment the tick to 2. This will trigger an update inside cDU. Flush
// the first update without flushing the second one.
instance.setState({tick: 2});
expect(Scheduler).toFlushAndYieldThrough([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
instance.setState({tick: 2});
});
// Now flush the cDU update.
expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
// TODO: why does this flush sync?
expect(Scheduler).toFlushAndYieldThrough([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
'render: 3',
'componentDidUpdate: 3',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
} else {
instance.setState({tick: 2});
expect(Scheduler).toFlushAndYieldThrough([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
// Now flush the cDU update.
expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
}
});
// @gate experimental || !enableSyncDefaultUpdates
it('performs Task work even after time runs out', () => {
class Foo extends React.Component {
state = {step: 1};
@@ -286,7 +333,14 @@ describe('ReactIncrementalScheduling', () => {
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
// This should be just enough to complete all the work, but not enough to
// commit it.
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
@@ -384,6 +384,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(ReactNoop.getChildren('portalContainer')).toEqual([]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('does not update child nodes if a flush is aborted', () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
@@ -409,7 +410,13 @@ describe('ReactIncrementalSideEffects', () => {
div(div(span('Hello'), span('Hello')), span('Yo')),
]);
ReactNoop.render(<Foo text="World" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo text="World" />);
});
} else {
ReactNoop.render(<Foo text="World" />);
}
// Flush some of the work without committing
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
@@ -633,12 +640,19 @@ describe('ReactIncrementalSideEffects', () => {
);
});
// @gate experimental || !enableSyncDefaultUpdates
it('can update a completed tree before it has a chance to commit', () => {
function Foo(props) {
Scheduler.unstable_yieldValue('Foo');
return <span prop={props.step} />;
}
ReactNoop.render(<Foo step={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={1} />);
});
} else {
ReactNoop.render(<Foo step={1} />);
}
// This should be just enough to complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
@@ -647,13 +661,26 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.flushNextYield();
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
ReactNoop.render(<Foo step={2} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={2} />);
});
} else {
ReactNoop.render(<Foo step={2} />);
}
// This should be just enough to complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
// This time, before we commit the tree, we update the root component with
// new props
ReactNoop.render(<Foo step={3} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo step={3} />);
});
} else {
ReactNoop.render(<Foo step={3} />);
}
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
// Now let's commit. We already had a commit that was pending, which will
// render 2.
@@ -61,9 +61,16 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYieldThrough(['commit']);
expect(state).toEqual({a: 'a'});
expect(Scheduler).toFlushWithoutYielding();
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// TODO: should deferredUpdates flush sync with the default update?
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
expect(Scheduler).toFlushWithoutYielding();
} else {
expect(state).toEqual({a: 'a'});
expect(Scheduler).toFlushWithoutYielding();
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
}
});
it('applies updates with equal priority in insertion order', () => {
@@ -130,6 +137,7 @@ describe('ReactIncrementalUpdates', () => {
expect(instance.state).toEqual({c: 'c', d: 'd'});
});
// @gate experimental || !enableSyncDefaultUpdates
it('can abort an update, schedule additional updates, and resume', () => {
let instance;
class Foo extends React.Component {
@@ -159,33 +167,75 @@ describe('ReactIncrementalUpdates', () => {
}
// Schedule some async updates
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
});
} else {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
}
// Begin the updates but don't flush them yet
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
expect(ReactNoop.getChildren()).toEqual([span('')]);
// Schedule some more updates at different priorities
instance.setState(createUpdate('d'));
ReactNoop.flushSync(() => {
instance.setState(createUpdate('e'));
instance.setState(createUpdate('f'));
});
instance.setState(createUpdate('g'));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
instance.setState(createUpdate('d'));
ReactNoop.flushSync(() => {
instance.setState(createUpdate('e'));
instance.setState(createUpdate('f'));
});
React.unstable_startTransition(() => {
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')]);
// 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')]);
// 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.
// TODO: should d, e, f be flushed again first?
expect(Scheduler).toFlushAndYield([
'd',
'e',
'f',
'a',
'b',
'c',
'd',
'e',
'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')]);
}
});
// @gate experimental || !enableSyncDefaultUpdates
it('can abort an update, schedule a replaceState, and resume', () => {
let instance;
class Foo extends React.Component {
@@ -215,34 +265,79 @@ describe('ReactIncrementalUpdates', () => {
}
// Schedule some async updates
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
});
} else {
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
}
// Begin the updates but don't flush them yet
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
expect(ReactNoop.getChildren()).toEqual([span('')]);
// Schedule some more updates at different priorities{
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'));
// Schedule some more updates at different priorities
if (gate(flags => flags.enableSyncDefaultUpdates)) {
instance.setState(createUpdate('d'));
// 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')]);
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.unstable_startTransition(() => {
instance.setState(createUpdate('g'));
});
// 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')]);
// The sync updates should have flushed, but not the async ones.
// TODO: should 'd' have flushed?
// TODO: should 'f' have flushed? I don't know what enqueueReplaceState is.
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([
'd',
'e',
'f',
'a',
'b',
'c',
'd',
'e',
'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')]);
}
});
it('passes accumulation of previous updates to replaceState updater function', () => {
@@ -459,6 +554,7 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop.getChildren()).toEqual([span('derived state')]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('regression: does not expire soon due to layout effects in the last batch', () => {
const {useState, useLayoutEffect} = React;
@@ -475,7 +571,13 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.act(() => {
ReactNoop.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
}
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
expect(Scheduler).toFlushAndYield([
@@ -485,13 +587,19 @@ describe('ReactIncrementalUpdates', () => {
]);
Scheduler.unstable_advanceTime(10000);
setCount(2);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
setCount(2);
});
} else {
setCount(2);
}
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
});
});
// @gate experimental || !enableSyncDefaultUpdates
it('regression: does not expire soon due to previous flushSync', () => {
function Text({text}) {
Scheduler.unstable_yieldValue(text);
@@ -505,29 +613,49 @@ describe('ReactIncrementalUpdates', () => {
Scheduler.unstable_advanceTime(10000);
ReactNoop.render(<Text text="B" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Text text="B" />);
});
} else {
ReactNoop.render(<Text text="B" />);
}
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('regression: does not expire soon due to previous expired work', () => {
function Text({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
ReactNoop.render(<Text text="A" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Text text="A" />);
});
} else {
ReactNoop.render(<Text text="A" />);
}
Scheduler.unstable_advanceTime(10000);
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded(['A']);
Scheduler.unstable_advanceTime(10000);
ReactNoop.render(<Text text="B" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Text text="B" />);
});
} else {
ReactNoop.render(<Text text="B" />);
}
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
const {useState, useLayoutEffect} = React;
@@ -560,7 +688,13 @@ describe('ReactIncrementalUpdates', () => {
expect(root).toMatchRenderedOutput('');
await ReactNoop.act(async () => {
pushToLog('A');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
pushToLog('A');
});
} else {
pushToLog('A');
}
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
pushToLog('B'),
@@ -584,6 +718,7 @@ describe('ReactIncrementalUpdates', () => {
expect(root).toMatchRenderedOutput('ABCD');
});
// @gate experimental || !enableSyncDefaultUpdates
it('when rebasing, does not exclude updates that were already committed, regardless of priority (classes)', async () => {
let pushToLog;
class App extends React.Component {
@@ -615,7 +750,13 @@ describe('ReactIncrementalUpdates', () => {
expect(root).toMatchRenderedOutput('');
await ReactNoop.act(async () => {
pushToLog('A');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
pushToLog('A');
});
} else {
pushToLog('A');
}
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
pushToLog('B'),
);
@@ -22,6 +22,7 @@ describe('ReactInterleavedUpdates', () => {
return text;
}
// @gate experimental || !enableSyncDefaultUpdates
test('update during an interleaved event is not processed during the current render', async () => {
const updaters = [];
@@ -55,13 +56,25 @@ describe('ReactInterleavedUpdates', () => {
expect(root).toMatchRenderedOutput('000');
await ReactNoop.act(async () => {
updateChildren(1);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateChildren(1);
});
} else {
updateChildren(1);
}
// Partially render the children. Only the first one.
expect(Scheduler).toFlushAndYieldThrough([1]);
// In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet.
updateChildren(2);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateChildren(2);
});
} else {
updateChildren(2);
}
// We should continue rendering without including the interleaved updates.
expect(Scheduler).toFlushUntilNextPaint([1, 1]);
@@ -73,6 +86,7 @@ describe('ReactInterleavedUpdates', () => {
});
// @gate experimental
// @gate !enableSyncDefaultUpdates
test('low priority update during an interleaved event is not processed during the current render', async () => {
// Same as previous test, but the interleaved update is lower priority than
// the in-progress render.
@@ -1475,6 +1475,7 @@ describe('ReactLazy', () => {
});
// @gate enableLazyElements
// @gate experimental || !enableSyncDefaultUpdates
it('mount and reorder lazy elements', async () => {
class Child extends React.Component {
componentDidMount() {
@@ -1534,7 +1535,13 @@ describe('ReactLazy', () => {
expect(root).toMatchRenderedOutput('AB');
// Swap the position of A and B
root.update(<Parent swap={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.update(<Parent swap={true} />);
});
} else {
root.update(<Parent swap={true} />);
}
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
await lazyChildB2;
// We need to flush to trigger the second one to load.
@@ -828,6 +828,7 @@ describe('ReactNewContext', () => {
);
});
// @gate experimental || !enableSyncDefaultUpdates
it('warns if multiple renderers concurrently render the same context', () => {
spyOnDev(console, 'error');
const Context = React.createContext(0);
@@ -846,7 +847,13 @@ describe('ReactNewContext', () => {
);
}
ReactNoop.render(<App value={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App value={1} />);
});
} else {
ReactNoop.render(<App value={1} />);
}
// Render past the Provider, but don't commit yet
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
@@ -88,6 +88,7 @@ describe('ReactSchedulerIntegration', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('requests a paint after committing', () => {
const scheduleCallback = Scheduler.unstable_scheduleCallback;
@@ -100,7 +101,13 @@ describe('ReactSchedulerIntegration', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
// Schedule a React render. React will request a paint after committing it.
root.render('Update');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render('Update');
});
} else {
root.render('Update');
}
// Advance time just to be sure the next tasks have lower priority
Scheduler.unstable_advanceTime(2000);
@@ -99,6 +99,7 @@ describe('ReactSuspense', () => {
}
}
// @gate experimental || !enableSyncDefaultUpdates
it('suspends rendering and continues later', () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
@@ -129,7 +130,13 @@ describe('ReactSuspense', () => {
// Navigate the shell to now render the child content.
// This should suspend.
root.update(<Foo renderBar={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.update(<Foo renderBar={true} />);
});
} else {
root.update(<Foo renderBar={true} />);
}
expect(Scheduler).toFlushAndYield([
'Foo',
@@ -197,6 +204,7 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AB');
});
// @gate experimental || !enableSyncDefaultUpdates
it('interrupts current render if promise resolves before current render phase', () => {
let didResolve = false;
const listeners = [];
@@ -238,15 +246,29 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Initial');
// The update will suspend.
root.update(
<>
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
<Text text="After Suspense" />
<Text text="Sibling" />
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.update(
<>
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
<Text text="After Suspense" />
<Text text="Sibling" />
</>,
);
});
} else {
root.update(
<>
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
<Text text="After Suspense" />
<Text text="Sibling" />
</>,
);
}
// Yield past the Suspense boundary but don't complete the last sibling.
expect(Scheduler).toFlushAndYieldThrough([
@@ -271,6 +293,7 @@ describe('ReactSuspense', () => {
});
// @gate experimental
// @gate !enableSyncDefaultUpdates
it(
'interrupts current render when something suspends with a ' +
"delay and we've already skipped over a lower priority update in " +
@@ -1274,7 +1274,13 @@ describe('ReactSuspenseList', () => {
}
// This render is only CPU bound. Nothing suspends.
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
expect(Scheduler).toFlushAndYieldThrough(['A']);
@@ -1452,7 +1458,13 @@ describe('ReactSuspenseList', () => {
}
// This render is only CPU bound. Nothing suspends.
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
expect(Scheduler).toFlushAndYieldThrough(['A']);
@@ -2449,7 +2461,13 @@ describe('ReactSuspenseList', () => {
await ReactNoop.act(async () => {
// Add a few items at the end.
updateLowPri(true);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
updateLowPri(true);
});
} else {
updateLowPri(true);
}
// Flush partially through.
expect(Scheduler).toFlushAndYieldThrough(['B', 'C']);
@@ -2586,7 +2604,13 @@ describe('ReactSuspenseList', () => {
);
}
ReactNoop.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
}
expect(Scheduler).toFlushAndYieldThrough([
'App',
@@ -2653,7 +2677,13 @@ describe('ReactSuspenseList', () => {
);
}
ReactNoop.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
}
expect(Scheduler).toFlushAndYieldThrough([
'App',
@@ -211,7 +211,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
}
ReactNoop.render(<Foo />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
}
expect(Scheduler).toFlushAndYieldThrough([
'Foo',
'Bar',
@@ -277,7 +283,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Foo']);
// The update will suspend.
ReactNoop.render(<Foo renderBar={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo renderBar={true} />);
});
} else {
ReactNoop.render(<Foo renderBar={true} />);
}
expect(Scheduler).toFlushAndYield([
'Foo',
'Bar',
@@ -341,14 +353,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
expect(Scheduler).toFlushAndYield([]);
// B suspends. Continue rendering the remaining siblings.
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
<AsyncText text="B" />
<Text text="C" />
<Text text="D" />
</Suspense>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
<AsyncText text="B" />
<Text text="C" />
<Text text="D" />
</Suspense>,
);
});
} else {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
<AsyncText text="B" />
<Text text="C" />
<Text text="D" />
</Suspense>,
);
}
// B suspends. Continue rendering the remaining siblings.
expect(Scheduler).toFlushAndYield([
'A',
@@ -376,6 +401,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// TODO: Delete this feature flag.
// @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
// @gate enableCache
// @gate experimental || !enableSyncDefaultUpdates
it('retries on error', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -410,7 +436,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.render(<App renderContent={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App renderContent={true} />);
});
} else {
ReactNoop.render(<App renderContent={true} />);
}
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
@@ -535,6 +567,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate enableCache
// @gate experimental || !enableSyncDefaultUpdates
it('keeps working on lower priority work after being pinged', async () => {
// Advance the virtual time so that we're close to the edge of a bucket.
ReactNoop.expire(149);
@@ -552,14 +585,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.render(<App showA={true} showB={false} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App showA={true} showB={false} />);
});
} else {
ReactNoop.render(<App showA={true} showB={false} />);
}
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
// Advance React's virtual time by enough to fall into a new async bucket,
// but not enough to expire the suspense timeout.
ReactNoop.expire(120);
ReactNoop.render(<App showA={true} showB={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<App showA={true} showB={true} />);
});
} else {
ReactNoop.render(<App showA={true} showB={true} />);
}
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
@@ -602,6 +647,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// times model. Might not make sense in the new model.
// TODO: This test doesn't over what it was originally designed to test.
// Either rewrite or delete.
// @gate experimental || !enableSyncDefaultUpdates
it('tries each subsequent level after suspending', async () => {
const root = ReactNoop.createRoot();
@@ -636,17 +682,35 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update at several distinct expiration times
await ReactNoop.act(async () => {
root.render(<App step={1} shouldSuspend={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App step={1} shouldSuspend={true} />);
});
} else {
root.render(<App step={1} shouldSuspend={true} />);
}
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
root.render(<App step={2} shouldSuspend={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App step={2} shouldSuspend={true} />);
});
} else {
root.render(<App step={2} shouldSuspend={true} />);
}
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
root.render(<App step={3} shouldSuspend={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App step={3} shouldSuspend={true} />);
});
} else {
root.render(<App step={3} shouldSuspend={true} />);
}
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
@@ -864,6 +928,52 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
// @gate experimental
it('does not expire for transitions', async () => {
ReactNoop.render(
<Fragment>
<Suspense fallback={<Text text="Loading..." />} />
</Fragment>,
);
expect(Scheduler).toFlushAndYield([]);
React.unstable_startTransition(() => {
ReactNoop.render(
<Fragment>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>
<Text text="Sync" />
</Fragment>,
);
});
expect(Scheduler).toFlushAndYield([
// The async child suspends
'Suspend! [Async]',
'Loading...',
// Continue on the sibling
'Sync',
]);
// The update hasn't expired yet, so we commit nothing.
expect(ReactNoop.getChildren()).toEqual([]);
// Advance both React's virtual time and Jest's timers,
// but not by enough to flush the promise or reach the true expiration time.
ReactNoop.expire(2000);
await advanceTimers(2000);
expect(ReactNoop.getChildren()).toEqual([]);
// Even flushing won't yield a fallback in a transition.
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([]);
// Once the promise resolves, we render the suspended view
await resolveText('Async');
expect(Scheduler).toFlushAndYield(['Async', 'Sync']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
// @gate enableCache
it('resolves successfully even if fallback render is pending', async () => {
ReactNoop.render(
@@ -873,13 +983,25 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.render(
<>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>
</>,
);
});
} else {
ReactNoop.render(
<>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>
</>,
);
}
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
await resolveText('Async');
@@ -928,11 +1050,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
expect(Scheduler).toFlushAndYield([]);
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>,
);
});
} else {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>,
);
}
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
@@ -1725,6 +1857,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate enableCache
// @gate experimental || !enableSyncDefaultUpdates
it('suspends for longer if something took a long (CPU bound) time to render', async () => {
function Foo({renderContent}) {
Scheduler.unstable_yieldValue('Foo');
@@ -1738,7 +1871,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['Foo']);
ReactNoop.render(<Foo renderContent={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />);
}
Scheduler.unstable_advanceTime(100);
await advanceTimers(100);
// Start rendering
@@ -1767,12 +1906,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await advanceTimers(500);
// No need to rerender.
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Since this is a transition, we never fallback.
expect(ReactNoop.getChildren()).toEqual([]);
} else {
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
}
// Flush the promise completely
await resolveText('A');
// Renders successfully
expect(Scheduler).toFlushAndYield(['A']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// TODO: Why does this render Foo
expect(Scheduler).toFlushAndYield(['Foo', 'A']);
} else {
expect(Scheduler).toFlushAndYield(['A']);
}
expect(ReactNoop.getChildren()).toEqual([span('A')]);
});
@@ -1913,7 +2062,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['Foo']);
ReactNoop.render(<Foo renderContent={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
// Advance some time.
@@ -1938,7 +2093,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// updates as way earlier in the past. This test ensures that we don't
// use this assumption to add a very long JND.
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Transitions never fallback.
expect(ReactNoop.getChildren()).toEqual([]);
} else {
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
}
});
// TODO: flip to "warns" when this is implemented again.
@@ -2223,7 +2383,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Foo', 'A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
ReactNoop.render(<Foo showB={true} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo showB={true} />);
});
} else {
ReactNoop.render(<Foo showB={true} />);
}
expect(Scheduler).toFlushAndYield([
'Foo',
@@ -2238,7 +2404,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(600);
await advanceTimers(600);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Transitions never fall back.
expect(ReactNoop.getChildren()).toEqual([span('A')]);
} else {
expect(ReactNoop.getChildren()).toEqual([
span('A'),
span('Loading B...'),
]);
}
});
// @gate enableCache
@@ -2757,14 +2931,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await ReactNoop.act(async () => {
// Update. Since showing a fallback would hide content that's already
// visible, it should suspend for a JND without committing.
root.render(<App text="First update" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App text="First update" />);
});
} else {
root.render(<App text="First update" />);
}
expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
// Should not display a fallback
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
// Update again. This should also suspend for a JND.
root.render(<App text="Second update" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App text="Second update" />);
});
} else {
root.render(<App text="Second update" />);
}
expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
// Should not display a fallback
@@ -3540,6 +3726,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// @gate experimental
// @gate enableCache
// @gate !enableSyncDefaultUpdates
it('regression: ping at high priority causes update to be dropped', async () => {
const {useState, unstable_useTransition: useTransition} = React;
@@ -452,7 +452,11 @@ describe('updaters', () => {
};
const LowPriorityUpdater = () => {
const [count, setCount] = React.useState(0);
triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1);
triggerLowPriorityUpdate = () => {
React.unstable_startTransition(() => {
setCount(prevCount => prevCount + 1);
});
};
Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`);
return <Yield value={`LowPriority ${count}`} />;
};
@@ -13,12 +13,19 @@
// This test is *.internal so that it can import this shared file.
import ReactVersion from 'shared/ReactVersion';
// Hard-coding because importing will not work with bundle tests and to
// avoid leaking exports for lanes that are only imported in this test.
const ReactFiberLane = {
SyncLane: /* */ 0b0000000000000000000000000000001,
DefaultLane: /* */ 0b0000000000000000000000000010000,
TransitionLane1: /* */ 0b0000000000000000000000001000000,
};
describe('SchedulingProfiler', () => {
let React;
let ReactTestRenderer;
let ReactNoop;
let Scheduler;
let ReactFiberLane;
let clearedMarks;
let featureDetectionMarkName = null;
@@ -82,11 +89,6 @@ describe('SchedulingProfiler', () => {
const SchedulingProfiler = require('react-reconciler/src/SchedulingProfiler');
formatLanes = SchedulingProfiler.formatLanes;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFiberLane = ReactFeatureFlags.enableNewReconciler
? require('react-reconciler/src/ReactFiberLane.new')
: require('react-reconciler/src/ReactFiberLane.old');
});
afterEach(() => {
@@ -147,6 +149,7 @@ describe('SchedulingProfiler', () => {
});
// @gate enableSchedulingProfiler
// @gate experimental || !enableSyncDefaultUpdates
it('should mark render yields', async () => {
function Bar() {
Scheduler.unstable_yieldValue('Bar');
@@ -158,16 +161,33 @@ describe('SchedulingProfiler', () => {
return <Bar />;
}
ReactNoop.render(<Foo />);
// Do one step of work.
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(<Foo />);
});
expectMarksToEqual([
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--render-yield',
]);
// Do one step of work.
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
expectMarksToEqual([
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`,
`--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`,
'--render-yield',
]);
} else {
ReactNoop.render(<Foo />);
// Do one step of work.
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
expectMarksToEqual([
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--render-yield',
]);
}
});
// @gate enableSchedulingProfiler
@@ -168,10 +168,18 @@ describe('SchedulingProfiler labels', () => {
event.initEvent('mouseover', true, true);
dispatchAndSetCurrentEvent(targetRef.current, event);
});
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.InputContinuousLane,
)}-App`,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.DefaultLane,
)}-App`,
);
} else {
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.InputContinuousLane,
)}-App`,
);
}
});
});
@@ -213,24 +213,45 @@ describe('useMutableSource', () => {
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
} else {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
}
// Do enough work to read from one component
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
@@ -431,7 +452,13 @@ describe('useMutableSource', () => {
// Changing values should schedule an update with React.
// Start working on this update but don't finish it.
source.value = 'two';
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
source.value = 'two';
});
} else {
source.value = 'two';
}
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
// Re-renders that occur before the update is processed
@@ -695,29 +722,57 @@ describe('useMutableSource', () => {
// Because the store has not changed yet, there are no pending updates,
// so it is considered safe to read from when we start this render.
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="b"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
<Component
label="c"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="b"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
<Component
label="c"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
} else {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="b"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
<Component
label="c"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
}
expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
// Mutating the source should trigger a tear detection on the next read,
@@ -808,22 +863,43 @@ describe('useMutableSource', () => {
act(() => {
// Start a render that uses the mutable source.
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
// Mutate source
@@ -1457,13 +1533,25 @@ describe('useMutableSource', () => {
expect(root).toMatchRenderedOutput('a0');
await act(async () => {
root.render(
<>
<Read getSnapshot={getSnapshotA} />
<Read getSnapshot={getSnapshotB} />
<Text text="c" />
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(
<>
<Read getSnapshot={getSnapshotA} />
<Read getSnapshot={getSnapshotB} />
<Text text="c" />
</>,
);
});
} else {
root.render(
<>
<Read getSnapshot={getSnapshotA} />
<Read getSnapshot={getSnapshotB} />
<Text text="c" />
</>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
// Mutate in an event. This schedules a subscription update on a, which
@@ -1481,10 +1569,16 @@ describe('useMutableSource', () => {
mutateB('b0');
});
// Finish the current render
expect(Scheduler).toFlushUntilNextPaint(['c']);
// a0 will re-render because of the mutation update. But it should show
// the latest value, not the intermediate one, to avoid tearing with b.
expect(Scheduler).toFlushUntilNextPaint(['a0']);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Default sync will flush both without yielding
expect(Scheduler).toFlushUntilNextPaint(['c', 'a0']);
} else {
expect(Scheduler).toFlushUntilNextPaint(['c']);
// a0 will re-render because of the mutation update. But it should show
// the latest value, not the intermediate one, to avoid tearing with b.
expect(Scheduler).toFlushUntilNextPaint(['a0']);
}
expect(root).toMatchRenderedOutput('a0b0c');
// We should be done.
expect(Scheduler).toFlushAndYield([]);
@@ -1591,7 +1685,13 @@ describe('useMutableSource', () => {
await act(async () => {
// Switch the parent and the child to read using the same config
root.render(<App parentConfig={configB} childConfig={configB} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App parentConfig={configB} childConfig={configB} />);
});
} else {
root.render(<App parentConfig={configB} childConfig={configB} />);
}
// Start rendering the parent, but yield before rendering the child
expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
@@ -1602,25 +1702,41 @@ describe('useMutableSource', () => {
source.valueB = '3';
});
expect(Scheduler).toFlushAndYieldThrough([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// In default sync mode, all of the updates flush sync.
expect(Scheduler).toFlushAndYieldThrough([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
'Parent: 3',
'Child: 3',
]);
// Now there are two pending mutations at different priorities. But they
// both read the same version of the mutable source, so we must render
// them simultaneously.
//
expect(Scheduler).toFlushAndYieldThrough([
'Parent: 3',
// Demonstrates that we can yield here
]);
expect(Scheduler).toFlushAndYield([
// Now finish the rest of the update
'Child: 3',
'Commit: 3, 3',
]);
expect(Scheduler).toFlushAndYield([
// Now finish the rest of the update
'Commit: 3, 3',
]);
} else {
expect(Scheduler).toFlushAndYieldThrough([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
]);
// Now there are two pending mutations at different priorities. But they
// both read the same version of the mutable source, so we must render
// them simultaneously.
//
expect(Scheduler).toFlushAndYieldThrough([
'Parent: 3',
// Demonstrates that we can yield here
]);
expect(Scheduler).toFlushAndYield([
// Now finish the rest of the update
'Child: 3',
'Commit: 3, 3',
]);
}
});
});
@@ -1855,22 +1971,43 @@ describe('useMutableSource', () => {
act(() => {
// Start a render that uses the mutable source.
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
const PrevScheduler = Scheduler;
@@ -1915,22 +2052,43 @@ describe('useMutableSource', () => {
act(() => {
// Start a render that uses the mutable source.
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
const PrevScheduler = Scheduler;
@@ -262,7 +262,13 @@ describe('useMutableSourceHydration', () => {
});
expect(() => {
act(() => {
root.render(<TestComponent />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<TestComponent />);
});
} else {
root.render(<TestComponent />);
}
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
source.value = 'two';
});
@@ -316,22 +322,43 @@ describe('useMutableSourceHydration', () => {
});
expect(() => {
act(() => {
root.render(
<>
<Component
label="0"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="1"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(
<>
<Component
label="0"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="1"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
);
});
} else {
root.render(
<>
<Component
label="0"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="1"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
source.valueB = 'b:two';
});
@@ -344,6 +371,7 @@ describe('useMutableSourceHydration', () => {
});
// @gate experimental
// @gate !enableSyncDefaultUpdates
it('should detect a tear during a higher priority interruption', () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -386,7 +414,13 @@ describe('useMutableSourceHydration', () => {
expect(() => {
act(() => {
root.render(<TestComponent flag={1} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<TestComponent flag={1} />);
});
} else {
root.render(<TestComponent flag={1} />);
}
expect(Scheduler).toFlushAndYieldThrough([1]);
// Render an update which will be higher priority than the hydration.
@@ -73,6 +73,7 @@ describe('ReactTestRendererAsync', () => {
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('flushThrough flushes until the expected values is yielded', () => {
function Child(props) {
Scheduler.unstable_yieldValue(props.children);
@@ -87,9 +88,19 @@ describe('ReactTestRendererAsync', () => {
</>
);
}
const renderer = ReactTestRenderer.create(<Parent step={1} />, {
unstable_isConcurrent: true,
});
let renderer;
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer = ReactTestRenderer.create(<Parent step={1} />, {
unstable_isConcurrent: true,
});
});
} else {
renderer = ReactTestRenderer.create(<Parent step={1} />, {
unstable_isConcurrent: true,
});
}
// Flush the first two siblings
expect(Scheduler).toFlushAndYieldThrough(['A:1', 'B:1']);
@@ -101,6 +112,7 @@ describe('ReactTestRendererAsync', () => {
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
});
// @gate experimental || !enableSyncDefaultUpdates
it('supports high priority interruptions', () => {
function Child(props) {
Scheduler.unstable_yieldValue(props.children);
@@ -124,9 +136,18 @@ describe('ReactTestRendererAsync', () => {
}
}
const renderer = ReactTestRenderer.create(<Example step={1} />, {
unstable_isConcurrent: true,
});
let renderer;
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer = ReactTestRenderer.create(<Example step={1} />, {
unstable_isConcurrent: true,
});
});
} else {
renderer = ReactTestRenderer.create(<Example step={1} />, {
unstable_isConcurrent: true,
});
}
// Flush the some of the changes, but don't commit
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
@@ -123,11 +123,21 @@ describe('ReactDOMTracing', () => {
SchedulerTracing.unstable_trace('initialization', 0, () => {
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
act(() => {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
});
} else {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
}
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
@@ -140,6 +150,7 @@ describe('ReactDOMTracing', () => {
);
expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
expect(onRender).toHaveBeenCalledTimes(2);
expect(onRender).toHaveLastRenderedWithInteractions(
new Set([interaction]),
@@ -275,11 +286,21 @@ describe('ReactDOMTracing', () => {
SchedulerTracing.unstable_trace('initialization', 0, () => {
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
act(() => {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
});
} else {
root.render(
<React.Profiler id="test" onRender={onRender}>
<App />
</React.Profiler>,
);
}
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
@@ -307,16 +328,13 @@ describe('ReactDOMTracing', () => {
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);
if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) {
expect(onRender).toHaveBeenCalledTimes(3);
} else {
// TODO: This is 4 instead of 3 because this update was scheduled at
// idle priority, and idle updates are slightly higher priority than
// offscreen work. So it takes two render passes to finish it. Profiler
// calls `onRender` for the first render even though everything
// bails out.
expect(onRender).toHaveBeenCalledTimes(4);
}
// TODO: This is 4 instead of 3 because this update was scheduled at
// idle priority, and idle updates are slightly higher priority than
// offscreen work. So it takes two render passes to finish it. Profiler
// calls `onRender` for the first render even though everything
// bails out.
expect(onRender).toHaveBeenCalledTimes(4);
expect(onRender).toHaveLastRenderedWithInteractions(
new Set([interaction]),
);
@@ -503,7 +521,13 @@ describe('ReactDOMTracing', () => {
scheduleUpdateWithHidden(),
);
});
scheduleUpdate();
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
scheduleUpdate();
});
} else {
scheduleUpdate();
}
expect(interaction).not.toBeNull();
expect(onRender).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -580,7 +604,13 @@ describe('ReactDOMTracing', () => {
SchedulerTracing.unstable_trace('initialization', 0, () => {
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
// This render is only CPU bound. Nothing suspends.
root.render(<App />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
}
});
expect(Scheduler).toFlushAndYieldThrough(['A']);
@@ -246,6 +246,7 @@ describe('Profiler', () => {
expect(callback).toHaveBeenCalledTimes(2);
});
// @gate experimental || !enableSyncDefaultUpdates
it('is not invoked until the commit phase', () => {
const callback = jest.fn();
@@ -254,15 +255,29 @@ describe('Profiler', () => {
return null;
};
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
<Yield value="last" />
</React.Profiler>,
{
unstable_isConcurrent: true,
},
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
<Yield value="last" />
</React.Profiler>,
{
unstable_isConcurrent: true,
},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
<Yield value="last" />
</React.Profiler>,
{
unstable_isConcurrent: true,
},
);
}
// Times are logged until a render is committed.
expect(Scheduler).toFlushAndYieldThrough(['first']);
@@ -796,6 +811,7 @@ describe('Profiler', () => {
});
describe('with regard to interruptions', () => {
// @gate experimental || !enableSyncDefaultUpdates
it('should accumulate actual time after a scheduling interruptions', () => {
const callback = jest.fn();
@@ -808,13 +824,25 @@ describe('Profiler', () => {
Scheduler.unstable_advanceTime(5); // 0 -> 5
// Render partially, but run out of time before completing.
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
<Yield renderTime={3} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
<Yield renderTime={3} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
<Yield renderTime={3} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
}
expect(Scheduler).toFlushAndYieldThrough(['Yield:2']);
expect(callback).toHaveBeenCalledTimes(0);
@@ -830,6 +858,7 @@ describe('Profiler', () => {
expect(call[5]).toBe(10); // commit time
});
// @gate experimental || !enableSyncDefaultUpdates
it('should not include time between frames', () => {
const callback = jest.fn();
@@ -843,16 +872,31 @@ describe('Profiler', () => {
// Render partially, but don't finish.
// This partial render should take 5ms of simulated time.
ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
<Yield renderTime={10} />
<React.Profiler id="inner" onRender={callback}>
<Yield renderTime={17} />
</React.Profiler>
</React.Profiler>,
{unstable_isConcurrent: true},
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
<Yield renderTime={10} />
<React.Profiler id="inner" onRender={callback}>
<Yield renderTime={17} />
</React.Profiler>
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
<Yield renderTime={10} />
<React.Profiler id="inner" onRender={callback}>
<Yield renderTime={17} />
</React.Profiler>
</React.Profiler>,
{unstable_isConcurrent: true},
);
}
expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0);
@@ -880,6 +924,7 @@ describe('Profiler', () => {
expect(outerCall[5]).toBe(87); // commit time
});
// @gate experimental || !enableSyncDefaultUpdates
it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
const callback = jest.fn();
@@ -893,13 +938,26 @@ describe('Profiler', () => {
// Render a partially update, but don't finish.
// This partial render should take 10ms of simulated time.
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
<Yield renderTime={20} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
let renderer;
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
<Yield renderTime={20} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
<Yield renderTime={20} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
}
expect(Scheduler).toFlushAndYieldThrough(['Yield:10']);
expect(callback).toHaveBeenCalledTimes(0);
@@ -933,6 +991,7 @@ describe('Profiler', () => {
expect(callback).toHaveBeenCalledTimes(0);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should report the expected times when a high-priority update replaces a low-priority update', () => {
const callback = jest.fn();
@@ -968,13 +1027,25 @@ describe('Profiler', () => {
// Render a partially update, but don't finish.
// This partial render should take 3ms of simulated time.
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
<Yield renderTime={5} />
<Yield renderTime={9} />
</React.Profiler>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
<Yield renderTime={5} />
<Yield renderTime={9} />
</React.Profiler>,
);
});
} else {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
<Yield renderTime={5} />
<Yield renderTime={9} />
</React.Profiler>,
);
}
expect(Scheduler).toFlushAndYieldThrough(['Yield:3']);
expect(callback).toHaveBeenCalledTimes(0);
@@ -1014,6 +1085,7 @@ describe('Profiler', () => {
expect(callback).toHaveBeenCalledTimes(1);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
const callback = jest.fn();
@@ -1080,7 +1152,13 @@ describe('Profiler', () => {
// Render a partially update, but don't finish.
// This partial render will take 10ms of actual render time.
first.setState({renderTime: 10});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
first.setState({renderTime: 10});
});
} else {
first.setState({renderTime: 10});
}
expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']);
expect(callback).toHaveBeenCalledTimes(0);
@@ -3343,6 +3421,7 @@ describe('Profiler', () => {
).toHaveBeenLastNotifiedOfInteraction(interaction);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should associate traced events with their subsequent commits', () => {
let instance = null;
@@ -3459,10 +3538,19 @@ describe('Profiler', () => {
interactionOne.name,
Scheduler.unstable_now(),
() => {
instance.setState({count: 1});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
instance.setState({count: 1});
// Update state again to verify our traced interaction isn't registered twice
instance.setState({count: 2});
// Update state again to verify our traced interaction isn't registered twice
instance.setState({count: 2});
});
} else {
instance.setState({count: 1});
// Update state again to verify our traced interaction isn't registered twice
instance.setState({count: 2});
}
// The scheduler/tracing package will notify of work started for the default thread,
// But React shouldn't notify until it's been flushed.
@@ -3655,6 +3743,7 @@ describe('Profiler', () => {
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
const onPostCommit = jest.fn(() => {
Scheduler.unstable_yieldValue('onPostCommit');
@@ -3715,7 +3804,13 @@ describe('Profiler', () => {
Scheduler.unstable_now(),
() => {
// Render a partially update, but don't finish.
first.setState({count: 1});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
first.setState({count: 1});
});
} else {
first.setState({count: 1});
}
expect(onWorkScheduled).toHaveBeenCalled();
expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
@@ -4590,6 +4685,7 @@ describe('Profiler', () => {
).toMatchInteraction(initialRenderInteraction);
});
// @gate experimental || !enableSyncDefaultUpdates
it('handles high-pri renderers between suspended and resolved (async) trees', async () => {
// Set up an initial shell. We need to set this up before the test sceanrio
// because we want initial render to suspend on navigation to the initial state.
@@ -4614,14 +4710,27 @@ describe('Profiler', () => {
initialRenderInteraction.name,
initialRenderInteraction.timestamp,
() => {
renderer.update(
<React.Profiler id="app" onCommit={onCommit}>
<React.Suspense fallback={<Text text="loading" />}>
<AsyncText text="loaded" ms={100} />
</React.Suspense>
<Text text="initial" />
</React.Profiler>,
);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer.update(
<React.Profiler id="app" onCommit={onCommit}>
<React.Suspense fallback={<Text text="loading" />}>
<AsyncText text="loaded" ms={100} />
</React.Suspense>
<Text text="initial" />
</React.Profiler>,
);
});
} else {
renderer.update(
<React.Profiler id="app" onCommit={onCommit}>
<React.Suspense fallback={<Text text="loading" />}>
<AsyncText text="loaded" ms={100} />
</React.Suspense>
<Text text="initial" />
</React.Profiler>,
);
}
},
);
expect(Scheduler).toFlushAndYield([
@@ -178,6 +178,7 @@ describe('ReactProfiler DevTools integration', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('regression test: #17159', () => {
function Text({text}) {
Scheduler.unstable_yieldValue(text);
@@ -195,7 +196,13 @@ describe('ReactProfiler DevTools integration', () => {
// for updates.
Scheduler.unstable_advanceTime(10000);
// Schedule an update.
root.update(<Text text="B" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
root.update(<Text text="B" />);
});
} else {
root.update(<Text text="B" />);
}
// Update B should not instantly expire.
expect(Scheduler).toFlushAndYieldThrough([]);
+2
View File
@@ -170,3 +170,5 @@ export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
@@ -60,6 +60,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
@@ -59,3 +59,4 @@ export const deletedTreeCleanUpLevel = __VARIANT__ ? 3 : 1;
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const enableLazyContextPropagation = __VARIANT__;
export const enableSyncDefaultUpdates = __VARIANT__;
@@ -34,6 +34,7 @@ export const {
disableSchedulerTimeoutInWorkLoop,
enableLazyContextPropagation,
deletedTreeCleanUpLevel,
enableSyncDefaultUpdates,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
@@ -262,6 +262,7 @@ describe('useSubscription', () => {
expect(subscriptions).toHaveLength(2);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should ignore values emitted by a new subscribable until the commit phase', () => {
const log = [];
@@ -331,7 +332,14 @@ describe('useSubscription', () => {
// Start React update, but don't finish
act(() => {
renderer.update(<Parent observed={observableB} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
@@ -362,6 +370,7 @@ describe('useSubscription', () => {
]);
});
// @gate experimental || !enableSyncDefaultUpdates
it('should not drop values emitted between updates', () => {
const log = [];
@@ -432,7 +441,13 @@ describe('useSubscription', () => {
// Start React update, but don't finish
act(() => {
renderer.update(<Parent observed={observableB} />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />);
}
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
expect(log).toEqual([]);
@@ -561,6 +576,7 @@ describe('useSubscription', () => {
Scheduler.unstable_flushAll();
});
// @gate experimental || !enableSyncDefaultUpdates
it('should not tear if a mutation occurs during a concurrent update', () => {
const input = document.createElement('input');
@@ -608,9 +624,21 @@ describe('useSubscription', () => {
// Interrupt with a second mutation "C" -> "D".
// This update will not be eagerly evaluated,
// but useSubscription() should eagerly close over the updated value to avoid tearing.
mutate('C');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
mutate('C');
});
} else {
mutate('C');
}
expect(Scheduler).toFlushAndYieldThrough(['render:first:C']);
mutate('D');
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.unstable_startTransition(() => {
mutate('D');
});
} else {
mutate('D');
}
expect(Scheduler).toFlushAndYield([
'render:second:C',
'render:first:D',