diff --git a/packages/create-subscription/src/__tests__/createSubscription-test.js b/packages/create-subscription/src/__tests__/createSubscription-test.js
index 66a8a652a831d..27075d7d7dee0 100644
--- a/packages/create-subscription/src/__tests__/createSubscription-test.js
+++ b/packages/create-subscription/src/__tests__/createSubscription-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js
index 1613360ce6369..950ec77bb1a6e 100644
--- a/packages/react-art/src/__tests__/ReactART-test.js
+++ b/packages/react-art/src/__tests__/ReactART-test.js
@@ -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);
diff --git a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
index 6c8d677cdbb93..03bac9c657bd6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
@@ -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
{hoverString}
;
+ }
+
+ await act(async () => {
+ root.render();
+ });
+ 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();
+ });
+ 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);
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index d37845c01b383..bc0df9fa6c3f4 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -1865,11 +1865,21 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
await act(async () => {
- root.render();
- 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();
+ });
+
+ expect(Scheduler).toFlushAndYieldThrough(['Before', 'After']);
+ } else {
+ root.render();
+
+ 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.
});
diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
index 3491aa7b94f1d..7d37d5b61e868 100644
--- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
@@ -1934,7 +1934,13 @@ describe('DOMPluginEventSystem', () => {
log.length = 0;
// Increase counter
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
// Yield before committing
expect(Scheduler).toFlushAndYieldThrough(['Test']);
diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js
index 32bb8139c7657..219c8cadb7f95 100644
--- a/packages/react-reconciler/src/ReactFiberLane.new.js
+++ b/packages/react-reconciler/src/ReactFiberLane.new.js
@@ -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;
diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js
index b577a67aa213f..4672d5a92c91c 100644
--- a/packages/react-reconciler/src/ReactFiberLane.old.js
+++ b/packages/react-reconciler/src/ReactFiberLane.old.js
@@ -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;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index b52680c91119b..df1b2653e204a 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -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.
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 08553e392818e..07f8a38b8bc0a 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -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.
diff --git a/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js b/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
index 1a46bed437305..599aabda72357 100644
--- a/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
@@ -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();
expect(Scheduler).toFlushAndYield([]);
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYield([
'Suspend! [A]',
'Suspend! [B]',
diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
index 5fc98d980e299..d251bb04f1115 100644
--- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYield([
'Suspend! [B]',
'Sibling',
diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
index 6326aaa2aeab6..a4c9fef937797 100644
--- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
// 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');
});
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
index 973a0cbea81d7..d01cd2a06537a 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
@@ -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();
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYield(['Suspend!']);
expect(root).toMatchRenderedOutput();
// Rendering again should suspend again.
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
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();
await ReactNoop.act(async () => {
- root.render();
- setLabel('B');
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ setLabel('B');
+ });
+ } else {
+ root.render();
+ setLabel('B');
+ }
expect(Scheduler).toFlushAndYield(['Suspend!']);
expect(root).toMatchRenderedOutput();
// Rendering again should suspend again.
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYield(['B:0']);
expect(root).toMatchRenderedOutput();
});
@@ -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(, () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(, () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ });
+ } else {
+ ReactNoop.render(, () =>
+ 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')]);
});
});
diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js
index fa7674d437700..e505d1e243fa4 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js
@@ -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 [, ];
}
- ReactNoop.render(, () => Scheduler.unstable_yieldValue('callback'));
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(, () =>
+ Scheduler.unstable_yieldValue('callback'),
+ );
+ });
+ } else {
+ ReactNoop.render(, () =>
+ 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();
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
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());
- 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(
-
-
-
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
+
+
+
+
+ ,
+ );
+ }
expect(Scheduler).toFlushAndYieldThrough(['Intl {}']);
ReactNoop.render(
@@ -1994,18 +2057,35 @@ describe('ReactIncremental', () => {
}
}
- ReactNoop.render(
-
-
-
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
-
+
-
-
-
- ,
- );
+
+
+
+
+
+ ,
+ );
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
// Interrupt at higher priority
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index df691e074b2a4..186085c843025 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -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(
-
-
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
-
-
-
+
+
+
+
+
-
- ,
- );
+ ,
+ );
+ }
// 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(
-
-
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
-
-
-
+
+
+
+
+
-
- ,
- );
+ ,
+ );
+ }
// 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(, onCommit);
+ React.unstable_startTransition(() => {
+ ReactNoop.render(, onCommit);
+ });
expect(Scheduler).toFlushAndYieldThrough(['error']);
- interrupt();
React.unstable_startTransition(() => {
// This update is in a separate batch
ReactNoop.render(, 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(, onCommit);
+ React.unstable_startTransition(() => {
+ ReactNoop.render(, onCommit);
+ });
expect(Scheduler).toFlushAndYieldThrough(['error']);
- interrupt();
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -323,12 +345,6 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.render(, 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(, () => Scheduler.unstable_yieldValue('commit'));
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(, () =>
+ Scheduler.unstable_yieldValue('commit'),
+ );
+ });
+ } else {
+ ReactNoop.render(, () =>
+ 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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(
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
+
+ ,
+ );
+ }
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(
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+
+
+ ,
+ );
+ }
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']);
@@ -1805,7 +1861,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
await ReactNoop.act(async () => {
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
// Render past the component that throws, then yield.
expect(Scheduler).toFlushAndYieldThrough(['Oops']);
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
index 0f41be8d56866..b372bc8fce25a 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
@@ -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 ;
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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 [, ];
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough([
['componentWillUpdate', hostDiv],
'render',
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
index 0fda09f04f08e..3126957b2318a 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
@@ -85,6 +85,7 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop).toMatchRenderedOutput();
});
+ // @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(, 'c');
- ReactNoop.renderToRootWithID(, 'b');
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.renderToRootWithID(, 'c');
+ ReactNoop.renderToRootWithID(, 'b');
+ });
+ } else {
+ ReactNoop.renderToRootWithID(, 'c');
+ ReactNoop.renderToRootWithID(, '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(, 'a');
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.renderToRootWithID(, 'a');
+ });
+ } else {
+ ReactNoop.renderToRootWithID(, '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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
+ 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();
+ // 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();
+ } else {
+ instance.setState({tick: 2});
+
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'render: 2',
+ 'componentDidUpdate: 2',
+ 'componentDidUpdate (before setState): 2',
+ 'componentDidUpdate (after setState): 2',
+ ]);
+ expect(ReactNoop).toMatchRenderedOutput();
+
+ // Now flush the cDU update.
+ expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
+ expect(ReactNoop).toMatchRenderedOutput();
+ }
});
+ // @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 ;
}
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
+
// This should be just enough to complete all the work, but not enough to
// commit it.
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
index 3939aa7e7003e..32d4699318a17 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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 ;
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// 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();
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// This should be just enough to complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
expect(ReactNoop.getChildrenAsJSX()).toEqual();
// This time, before we commit the tree, we update the root component with
// new props
- ReactNoop.render();
+
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(ReactNoop.getChildrenAsJSX()).toEqual();
// Now let's commit. We already had a commit that was pending, which will
// render 2.
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
index 98092cf33ffa8..9eaa95f93edf5 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
@@ -61,9 +61,16 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render();
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'));
-
- // 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')]);
+ // Schedule some more updates at different priorities
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ instance.setState(createUpdate('d'));
+
+ ReactNoop.flushSync(() => {
+ instance.setState(createUpdate('e'));
+ // No longer a public API, but we can test that it works internally by
+ // reaching into the updater.
+ instance.updater.enqueueReplaceState(instance, createUpdate('f'));
+ });
+ React.unstable_startTransition(() => {
+ instance.setState(createUpdate('g'));
+ });
+
+ // 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
Scheduler.unstable_advanceTime(10000);
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded(['A']);
Scheduler.unstable_advanceTime(10000);
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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'),
);
diff --git a/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js
index d21d39aacf431..271fe1ffb7c35 100644
--- a/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js
@@ -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.
diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
index b9e6b0c268061..215c83db2e25d 100644
--- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.update();
+ });
+ } else {
+ root.update();
+ }
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
await lazyChildB2;
// We need to flush to trigger the second one to load.
diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
index 4f64972715ac4..5c338f01db8e5 100644
--- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
// Render past the Provider, but don't commit yet
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
diff --git a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
index c0ab53a40dc08..d0fb1fec858c4 100644
--- a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
@@ -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);
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
index 9a9f57ccd20b4..ae3f286f542b3 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.update();
+ });
+ } else {
+ root.update();
+ }
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(
- <>
- }>
-
-
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.update(
+ <>
+ }>
+
+
+
+
+ >,
+ );
+ });
+ } else {
+ root.update(
+ <>
+ }>
+
+
+
+
+ >,
+ );
+ }
// 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 " +
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
index 2cdc2621fbe9a..dfac720d4b255 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
@@ -1274,7 +1274,13 @@ describe('ReactSuspenseList', () => {
}
// This render is only CPU bound. Nothing suspends.
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough(['A']);
@@ -1452,7 +1458,13 @@ describe('ReactSuspenseList', () => {
}
// This render is only CPU bound. Nothing suspends.
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough([
'App',
@@ -2653,7 +2677,13 @@ describe('ReactSuspenseList', () => {
);
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough([
'App',
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index 9d6056e25d3db..bb4e0f6892a6e 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -211,7 +211,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
}
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYieldThrough([
'Foo',
'Bar',
@@ -277,7 +283,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Foo']);
// The update will suspend.
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
expect(Scheduler).toFlushAndYield([
'Foo',
'Bar',
@@ -341,14 +353,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(} />);
expect(Scheduler).toFlushAndYield([]);
// B suspends. Continue rendering the remaining siblings.
- ReactNoop.render(
- }>
-
-
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ }>
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ }>
+
+
+
+
+ ,
+ );
+ }
// 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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();
@@ -864,22 +928,80 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
- // @gate enableCache
- it('resolves successfully even if fallback render is pending', async () => {
+ // @gate experimental
+ it('does not expire for transitions', async () => {
ReactNoop.render(
- <>
+
} />
- >,
+ ,
);
expect(Scheduler).toFlushAndYield([]);
+
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ 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(
<>
- }>
-
-
+ } />
>,
);
+ expect(Scheduler).toFlushAndYield([]);
+ expect(ReactNoop.getChildren()).toEqual([]);
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+ }>
+
+
+ >,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+ }>
+
+
+ >,
+ );
+ }
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
await resolveText('Async');
@@ -928,11 +1050,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(} />);
expect(Scheduler).toFlushAndYield([]);
- ReactNoop.render(
- }>
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ }>
+
+ ,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ }>
+
+ ,
+ );
+ }
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();
expect(Scheduler).toFlushAndYield(['Foo']);
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
expect(Scheduler).toFlushAndYield(['Foo']);
- ReactNoop.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+ } else {
+ ReactNoop.render();
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
// Should not display a fallback
expect(root).toMatchRenderedOutput();
// Update again. This should also suspend for a JND.
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
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;
diff --git a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js
index a2f6ed4bd04cb..8d762f87b8fef 100644
--- a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js
@@ -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 ;
};
diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
index 14f7f0412c9d4..4240b363330cd 100644
--- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
@@ -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 ;
}
- ReactNoop.render();
- // 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',
- ]);
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render();
+ });
+
+ // 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();
+
+ // 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
diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
index 6474be855b38f..da59c153f43dd 100644
--- a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
@@ -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`,
+ );
+ }
});
});
diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
index e9fc689850233..fd7ebbeb72c78 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
@@ -213,24 +213,45 @@ describe('useMutableSource', () => {
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
-
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => 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(
- <>
-
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+
+
+
+ >,
+ () => 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(
- <>
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ }
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
// Mutate source
@@ -1457,13 +1533,25 @@ describe('useMutableSource', () => {
expect(root).toMatchRenderedOutput('a0');
await act(async () => {
- root.render(
- <>
-
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render(
+ <>
+
+
+
+ >,
+ );
+ });
+ } else {
+ root.render(
+ <>
+
+
+
+ >,
+ );
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
// 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(
- <>
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ }
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(
- <>
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ });
+ } else {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ }
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
const PrevScheduler = Scheduler;
diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
index f737994ad7e70..c26ab9deba116 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
@@ -262,7 +262,13 @@ describe('useMutableSourceHydration', () => {
});
expect(() => {
act(() => {
- root.render();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
source.value = 'two';
});
@@ -316,22 +322,43 @@ describe('useMutableSourceHydration', () => {
});
expect(() => {
act(() => {
- root.render(
- <>
-
-
- >,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render(
+ <>
+
+
+ >,
+ );
+ });
+ } else {
+ root.render(
+ <>
+
+
+ >,
+ );
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
expect(Scheduler).toFlushAndYieldThrough([1]);
// Render an update which will be higher priority than the hydration.
diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
index f83cbb8820beb..1a4554573afd1 100644
--- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
@@ -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(, {
- unstable_isConcurrent: true,
- });
+
+ let renderer;
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+ });
+ } else {
+ renderer = ReactTestRenderer.create(, {
+ 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(, {
- unstable_isConcurrent: true,
- });
+ let renderer;
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+ });
+ } else {
+ renderer = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+ }
// Flush the some of the changes, but don't commit
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
index 249aeab173349..1a026d732497c 100644
--- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
@@ -123,11 +123,21 @@ describe('ReactDOMTracing', () => {
SchedulerTracing.unstable_trace('initialization', 0, () => {
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
act(() => {
- root.render(
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ } else {
+ root.render(
+
+
+ ,
+ );
+ }
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(
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ } else {
+ root.render(
+
+
+ ,
+ );
+ }
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.render();
+ });
+ } else {
+ root.render();
+ }
});
expect(Scheduler).toFlushAndYieldThrough(['A']);
diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js
index 6b0c02432544b..02c7541e38374 100644
--- a/packages/react/src/__tests__/ReactProfiler-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js
@@ -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(
-
-
-
- ,
- {
- unstable_isConcurrent: true,
- },
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactTestRenderer.create(
+
+
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+ });
+ } else {
+ ReactTestRenderer.create(
+
+
+
+ ,
+ {
+ 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(
-
-
-
- ,
- {unstable_isConcurrent: true},
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactTestRenderer.create(
+
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ } else {
+ ReactTestRenderer.create(
+
+
+
+ ,
+ {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(
-
-
-
-
-
-
- ,
- {unstable_isConcurrent: true},
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ ReactTestRenderer.create(
+
+
+
+
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ } else {
+ ReactTestRenderer.create(
+
+
+
+
+
+
+ ,
+ {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(
-
-
-
- ,
- {unstable_isConcurrent: true},
- );
+ let renderer;
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer = ReactTestRenderer.create(
+
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ } else {
+ renderer = ReactTestRenderer.create(
+
+
+
+ ,
+ {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(
-
-
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer.update(
+
+
+
+
+ ,
+ );
+ });
+ } else {
+ renderer.update(
+
+
+
+
+ ,
+ );
+ }
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(
-
- }>
-
-
-
- ,
- );
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer.update(
+
+ }>
+
+
+
+ ,
+ );
+ });
+ } else {
+ renderer.update(
+
+ }>
+
+
+
+ ,
+ );
+ }
},
);
expect(Scheduler).toFlushAndYield([
diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
index 33454da6649d2..bd3f168af4eeb 100644
--- a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ root.update();
+ });
+ } else {
+ root.update();
+ }
// Update B should not instantly expire.
expect(Scheduler).toFlushAndYieldThrough([]);
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 0729d53d7f3d0..c8c2375babc90 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -170,3 +170,5 @@ export const enableRecursiveCommitTraversal = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
+
+export const enableSyncDefaultUpdates = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 826afbc1c9e18..47a2ea6ee2ab5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 99fbcd61b5be1..18375388be0a5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 72a390ca9cd7c..d7605e5ebb44e 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index 16243e404611d..516d578b0d570 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 190d006b27fa5..7bbc30f7cb46f 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 61a48fbbd5bce..bd4137d3e7cb0 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index 71e77cf6971ab..dacb018e3bea6 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -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
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index 9b9ec4310c9c3..d0fc8e6177067 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -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__;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index eed614e4ae7ef..871d4cc48ff80 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -34,6 +34,7 @@ export const {
disableSchedulerTimeoutInWorkLoop,
enableLazyContextPropagation,
deletedTreeCleanUpLevel,
+ enableSyncDefaultUpdates,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
diff --git a/packages/use-subscription/src/__tests__/useSubscription-test.js b/packages/use-subscription/src/__tests__/useSubscription-test.js
index 6e0a9e5045661..cfb082dfd2f65 100644
--- a/packages/use-subscription/src/__tests__/useSubscription-test.js
+++ b/packages/use-subscription/src/__tests__/useSubscription-test.js
@@ -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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer.update();
+ });
+ } else {
+ renderer.update();
+ }
+
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();
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ React.unstable_startTransition(() => {
+ renderer.update();
+ });
+ } else {
+ renderer.update();
+ }
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',