From 933880b4544a83ce54c8a47f348effe725a58843 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 9 Apr 2021 19:50:09 -0400 Subject: [PATCH] Make time-slicing opt-in (#21072) * Add enableSyncDefaultUpdates feature flag * Add enableSyncDefaultUpdates implementation * Fix tests * Switch feature flag to true by default * Finish concurrent render whenever for non-sync lanes * Also return DefaultLane with eventLane * Gate interruption test * Add continuout native event test * Fix tests from rebasing main * Hardcode lanes, remove added export * Sync forks --- .../src/__tests__/createSubscription-test.js | 18 +- .../react-art/src/__tests__/ReactART-test.js | 1 + .../ReactDOMNativeEventHeuristic-test.js | 40 ++ ...DOMServerPartialHydration-test.internal.js | 20 +- .../DOMPluginEventSystem-test.internal.js | 8 +- .../src/ReactFiberLane.new.js | 2 +- .../src/ReactFiberLane.old.js | 2 +- .../src/ReactFiberWorkLoop.new.js | 36 +- .../src/ReactFiberWorkLoop.old.js | 36 +- ...asedOnReactExpirationTime-test.internal.js | 9 +- .../src/__tests__/ReactExpiration-test.js | 125 +++++- .../src/__tests__/ReactFlushSync-test.js | 26 +- .../ReactHooksWithNoopRenderer-test.js | 132 ++++-- .../src/__tests__/ReactIncremental-test.js | 179 ++++++-- ...tIncrementalErrorHandling-test.internal.js | 180 +++++--- .../ReactIncrementalReflection-test.js | 43 +- .../ReactIncrementalScheduling-test.js | 88 +++- .../ReactIncrementalSideEffects-test.js | 35 +- .../__tests__/ReactIncrementalUpdates-test.js | 243 ++++++++--- .../__tests__/ReactInterleavedUpdates-test.js | 18 +- .../src/__tests__/ReactLazy-test.internal.js | 9 +- .../src/__tests__/ReactNewContext-test.js | 9 +- .../ReactSchedulerIntegration-test.js | 9 +- .../__tests__/ReactSuspense-test.internal.js | 43 +- .../src/__tests__/ReactSuspenseList-test.js | 40 +- .../ReactSuspenseWithNoopRenderer-test.js | 261 ++++++++++-- .../__tests__/ReactUpdaters-test.internal.js | 6 +- .../SchedulingProfiler-test.internal.js | 52 ++- .../SchedulingProfilerLabels-test.internal.js | 18 +- .../useMutableSource-test.internal.js | 398 ++++++++++++------ .../useMutableSourceHydration-test.js | 70 ++- .../__tests__/ReactTestRendererAsync-test.js | 33 +- .../ReactDOMTracing-test.internal.js | 74 +++- .../__tests__/ReactProfiler-test.internal.js | 215 +++++++--- ...ofilerDevToolsIntegration-test.internal.js | 9 +- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + .../src/__tests__/useSubscription-test.js | 36 +- 46 files changed, 1996 insertions(+), 538 deletions(-) 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',