From da94e8b24a3f31a3e805f9bf6bba73055aad9d41 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 4 Apr 2023 10:08:14 -0400 Subject: [PATCH] Revert "Cleanup enableSyncDefaultUpdate flag (#26236)" (#26528) This reverts commit b2ae9ddb3b497d16a7c27c051da1827d08871138. While the feature flag is fully rolled out, these tests are also testing behavior set with an unstable flag on root, which for now we want to preserve. Not sure if there's a better way then adding a dynamic feature flag to the www build? --- .../react-art/src/__tests__/ReactART-test.js | 58 ++++ .../ReactDOMNativeEventHeuristic-test.js | 6 +- ...DOMServerPartialHydration-test.internal.js | 9 +- .../DOMPluginEventSystem-test.internal.js | 8 +- packages/react-reconciler/src/ReactFiber.js | 7 +- .../src/__tests__/ReactExpiration-test.js | 213 ++++++++---- .../src/__tests__/ReactFlushSync-test.js | 8 +- .../ReactHooksWithNoopRenderer-test.js | 102 ++++-- .../src/__tests__/ReactIncremental-test.js | 110 +++++-- ...tIncrementalErrorHandling-test.internal.js | 92 +++++- .../ReactIncrementalReflection-test.js | 40 ++- .../ReactIncrementalScheduling-test.js | 41 ++- .../ReactIncrementalSideEffects-test.js | 32 +- .../__tests__/ReactIncrementalUpdates-test.js | 130 ++++++-- .../__tests__/ReactInterleavedUpdates-test.js | 69 +++- .../src/__tests__/ReactLazy-test.internal.js | 8 +- .../src/__tests__/ReactNewContext-test.js | 16 +- .../ReactSchedulerIntegration-test.js | 8 +- .../__tests__/ReactSuspense-test.internal.js | 95 +++++- .../ReactSuspenseEffectsSemantics-test.js | 16 +- .../src/__tests__/ReactSuspenseList-test.js | 44 ++- .../ReactSuspenseWithNoopRenderer-test.js | 310 +++++++++++++++--- .../useMutableSource-test.internal.js | 199 +++++++++-- .../useMutableSourceHydration-test.js | 13 +- .../__tests__/ReactTestRendererAsync-test.js | 20 +- .../__tests__/ReactProfiler-test.internal.js | 83 ++++- ...ofilerDevToolsIntegration-test.internal.js | 8 +- packages/shared/ReactFeatureFlags.js | 3 + .../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 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + .../src/__tests__/useSubscription-test.js | 32 +- 36 files changed, 1475 insertions(+), 312 deletions(-) diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index 714e19d1cc..9f73006d02 100644 --- a/packages/react-art/src/__tests__/ReactART-test.js +++ b/packages/react-art/src/__tests__/ReactART-test.js @@ -33,12 +33,16 @@ const ReactTestRenderer = require('react-test-renderer'); // Isolate the noop renderer jest.resetModules(); +const ReactNoop = require('react-noop-renderer'); +const Scheduler = require('scheduler'); let Group; let Shape; let Surface; let TestComponent; +let waitFor; + const Missing = {}; function testDOMNodeStructure(domNode, expectedStructure) { @@ -76,6 +80,8 @@ describe('ReactART', () => { Shape = ReactART.Shape; Surface = ReactART.Surface; + ({waitFor} = require('internal-test-utils')); + TestComponent = class extends React.Component { group = React.createRef(); @@ -357,6 +363,58 @@ describe('ReactART', () => { doClick(instance); expect(onClick2).toBeCalled(); }); + + // @gate !enableSyncDefaultUpdates + it('can concurrently render with a "primary" renderer while sharing context', async () => { + const CurrentRendererContext = React.createContext(null); + + function Yield(props) { + Scheduler.log(props.value); + return null; + } + + let ops = []; + function LogCurrentRenderer() { + return ( + + {currentRenderer => { + ops.push(currentRenderer); + return null; + }} + + ); + } + + // Using test renderer instead of the DOM renderer here because async + // testing APIs for the DOM renderer don't exist. + ReactNoop.render( + + + + + + , + ); + + await waitFor(['A']); + + ReactDOM.render( + + + + + + , + container, + ); + + expect(ops).toEqual([null, 'ART']); + + ops = []; + await waitFor(['B', 'C']); + + expect(ops).toEqual(['Test']); + }); }); describe('ReactARTComponents', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js index 60c78035e2..3300dc1bb3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js @@ -312,7 +312,11 @@ describe('ReactDOMNativeEventHeuristic-test', () => { expect(container.textContent).toEqual('not hovered'); await waitFor(['hovered']); - expect(container.textContent).toEqual('hovered'); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(container.textContent).toEqual('hovered'); + } else { + expect(container.textContent).toEqual('not hovered'); + } }); expect(container.textContent).toEqual('hovered'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 498432186e..ab09c63a7b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2036,7 +2036,14 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; await act(async () => { - await waitFor(['Before', 'After']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + await waitFor(['Before', 'After']); + } else { + await waitFor(['Before']); + // This took a long time to render. + Scheduler.unstable_advanceTime(1000); + await waitFor(['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 1a1769e82c..4b6c5717fa 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -1984,9 +1984,13 @@ describe('DOMPluginEventSystem', () => { log.length = 0; // Increase counter - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } // Yield before committing await waitFor(['Test']); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index f06844533e..1e02b09441 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -33,6 +33,7 @@ import { enableProfilerTimer, enableScopeAPI, enableLegacyHidden, + enableSyncDefaultUpdates, allowConcurrentByDefault, enableTransitionTracing, enableDebugTracing, @@ -458,9 +459,11 @@ export function createHostRootFiber( mode |= StrictLegacyMode | StrictEffectsMode; } if ( + // We only use this flag for our repo tests to check both behaviors. + // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting" + !enableSyncDefaultUpdates || // Only for internal experiments. - allowConcurrentByDefault && - concurrentUpdatesByDefaultOverride + (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride) ) { mode |= ConcurrentUpdatesByDefaultMode; } diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js index 26a82e8be3..2c38a05168 100644 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js @@ -115,29 +115,54 @@ describe('ReactExpiration', () => { } } + function flushNextRenderIfExpired() { + // This will start rendering the next level of work. If the work hasn't + // expired yet, React will exit without doing anything. If it has expired, + // it will schedule a sync task. + Scheduler.unstable_flushExpired(); + // Flush the sync task. + ReactNoop.flushSync(); + } + it('increases priority of updates as time progresses', async () => { - ReactNoop.render(); - React.startTransition(() => { - ReactNoop.render(); - }); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + ReactNoop.render(); + React.startTransition(() => { + ReactNoop.render(); + }); + await waitFor(['Step 1']); - await waitFor(['Step 1']); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); - expect(ReactNoop).toMatchRenderedOutput('Step 1'); + // Nothing has expired yet because time hasn't advanced. + await unstable_waitForExpired([]); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); - // Nothing has expired yet because time hasn't advanced. - await unstable_waitForExpired([]); - expect(ReactNoop).toMatchRenderedOutput('Step 1'); + // Advance time a bit, but not enough to expire the low pri update. + ReactNoop.expire(4500); + await unstable_waitForExpired([]); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); - // Advance time a bit, but not enough to expire the low pri update. - ReactNoop.expire(4500); - await unstable_waitForExpired([]); - expect(ReactNoop).toMatchRenderedOutput('Step 1'); + // Advance by a little bit more. Now the update should expire and flush. + ReactNoop.expire(500); + await unstable_waitForExpired(['Step 2']); + expect(ReactNoop).toMatchRenderedOutput('Step 2'); + } else { + ReactNoop.render(); + expect(ReactNoop).toMatchRenderedOutput(null); - // Advance by a little bit more. Now the update should expire and flush. - ReactNoop.expire(500); - await unstable_waitForExpired(['Step 2']); - expect(ReactNoop).toMatchRenderedOutput('Step 2'); + // Nothing has expired yet because time hasn't advanced. + flushNextRenderIfExpired(); + expect(ReactNoop).toMatchRenderedOutput(null); + // Advance time a bit, but not enough to expire the low pri update. + ReactNoop.expire(4500); + flushNextRenderIfExpired(); + expect(ReactNoop).toMatchRenderedOutput(null); + // Advance by another second. Now the update should expire and flush. + ReactNoop.expire(500); + flushNextRenderIfExpired(); + expect(ReactNoop).toMatchRenderedOutput(); + } }); it('two updates of like priority in the same event always flush within the same batch', async () => { @@ -162,9 +187,13 @@ describe('ReactExpiration', () => { // First, show what happens for updates in two separate events. // Schedule an update. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Advance the timer. Scheduler.unstable_advanceTime(2000); // Partially flush the first update, then interrupt it. @@ -219,9 +248,13 @@ describe('ReactExpiration', () => { // First, show what happens for updates in two separate events. // Schedule an update. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Advance the timer. Scheduler.unstable_advanceTime(2000); // Partially flush the first update, then interrupt it. @@ -287,9 +320,13 @@ describe('ReactExpiration', () => { } // Initial mount - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll([ 'initial [A] [render]', 'initial [B] [render]', @@ -302,9 +339,13 @@ describe('ReactExpiration', () => { ]); // Partial update - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + subscribers.forEach(s => s.setState({text: '1'})); + }); + } else { subscribers.forEach(s => s.setState({text: '1'})); - }); + } await waitFor(['1 [A] [render]', '1 [B] [render]']); // Before the update can finish, update again. Even though no time has @@ -330,9 +371,13 @@ describe('ReactExpiration', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitFor(['A']); await waitFor(['B']); @@ -359,9 +404,13 @@ describe('ReactExpiration', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitFor(['A']); await waitFor(['B']); @@ -379,36 +428,62 @@ describe('ReactExpiration', () => { jest.resetModules(); Scheduler = require('scheduler'); - const InternalTestUtils = require('internal-test-utils'); - waitFor = InternalTestUtils.waitFor; - assertLog = InternalTestUtils.assertLog; - unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired; - // Before importing the renderer, advance the current time by a number - // larger than the maximum allowed for bitwise operations. - const maxSigned31BitInt = 1073741823; - Scheduler.unstable_advanceTime(maxSigned31BitInt * 100); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + const InternalTestUtils = require('internal-test-utils'); + waitFor = InternalTestUtils.waitFor; + assertLog = InternalTestUtils.assertLog; + unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired; - // Now import the renderer. On module initialization, it will read the - // current time. - ReactNoop = require('react-noop-renderer'); - React = require('react'); + // Before importing the renderer, advance the current time by a number + // larger than the maximum allowed for bitwise operations. + const maxSigned31BitInt = 1073741823; + Scheduler.unstable_advanceTime(maxSigned31BitInt * 100); - ReactNoop.render(); - React.startTransition(() => { - ReactNoop.render(); - }); - await waitFor(['Step 1']); + // Now import the renderer. On module initialization, it will read the + // current time. + ReactNoop = require('react-noop-renderer'); + React = require('react'); - // The update should not have expired yet. - await unstable_waitForExpired([]); + ReactNoop.render(); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + await waitFor(['Step 1']); + } else { + ReactNoop.render('Hi'); + } - expect(ReactNoop).toMatchRenderedOutput('Step 1'); + // The update should not have expired yet. + await unstable_waitForExpired([]); - // Advance the time some more to expire the update. - Scheduler.unstable_advanceTime(10000); - await unstable_waitForExpired(['Step 2']); - expect(ReactNoop).toMatchRenderedOutput('Step 2'); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); + + // Advance the time some more to expire the update. + Scheduler.unstable_advanceTime(10000); + await unstable_waitForExpired(['Step 2']); + expect(ReactNoop).toMatchRenderedOutput('Step 2'); + } else { + // Before importing the renderer, advance the current time by a number + // larger than the maximum allowed for bitwise operations. + const maxSigned31BitInt = 1073741823; + Scheduler.unstable_advanceTime(maxSigned31BitInt * 100); + // Now import the renderer. On module initialization, it will read the + // current time. + ReactNoop = require('react-noop-renderer'); + ReactNoop.render('Hi'); + + // The update should not have expired yet. + flushNextRenderIfExpired(); + await waitFor([]); + expect(ReactNoop).toMatchRenderedOutput(null); + // Advance the time some more to expire the update. + Scheduler.unstable_advanceTime(10000); + flushNextRenderIfExpired(); + await waitFor([]); + expect(ReactNoop).toMatchRenderedOutput('Hi'); + } }); it('should measure callback timeout relative to current time, not start-up time', async () => { @@ -419,9 +494,13 @@ describe('ReactExpiration', () => { // Before scheduling an update, advance the current time. Scheduler.unstable_advanceTime(10000); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render('Hi'); + }); + } else { ReactNoop.render('Hi'); - }); + } await unstable_waitForExpired([]); expect(ReactNoop).toMatchRenderedOutput(null); @@ -462,9 +541,13 @@ describe('ReactExpiration', () => { // First demonstrate what happens when there's no starvation await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + updateNormalPri(); + }); + } else { updateNormalPri(); - }); + } await waitFor(['Sync pri: 0']); updateSyncPri(); assertLog(['Sync pri: 1', 'Normal pri: 0']); @@ -482,9 +565,13 @@ describe('ReactExpiration', () => { // Do the same thing, but starve the first update await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + updateNormalPri(); + }); + } else { updateNormalPri(); - }); + } await waitFor(['Sync pri: 1']); // This time, a lot of time has elapsed since the normal pri update @@ -645,9 +732,13 @@ describe('ReactExpiration', () => { expect(root).toMatchRenderedOutput('A0BC'); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend! [A1]', 'Loading...']); // Lots of time elapses before the promise resolves diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js index e2d9ba7666..57e2aad2e3 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -49,9 +49,13 @@ describe('ReactFlushSync', () => { const root = ReactNoop.createRoot(); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } // This will yield right before the passive effect fires await waitForPaint(['0, 0']); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index c62d12075f..627dc4e856 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -179,10 +179,15 @@ describe('ReactHooksWithNoopRenderer', () => { // Schedule some updates await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + counter.current.updateCount(1); + counter.current.updateCount(count => count + 10); + }); + } else { counter.current.updateCount(1); counter.current.updateCount(count => count + 10); - }); + } // Partially flush without committing await waitFor(['Count: 11']); @@ -687,16 +692,24 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll([0]); expect(root).toMatchRenderedOutput(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend!']); expect(root).toMatchRenderedOutput(); // Rendering again should suspend again. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend!']); }); @@ -742,25 +755,38 @@ describe('ReactHooksWithNoopRenderer', () => { expect(root).toMatchRenderedOutput(); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + setLabel('B'); + }); + } else { root.render(); setLabel('B'); - }); + } await waitForAll(['Suspend!']); expect(root).toMatchRenderedOutput(); // Rendering again should suspend again. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend!']); // Flip the signal back to "cancel" the update. However, the update to // label should still proceed. It shouldn't have been dropped. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['B:0']); expect(root).toMatchRenderedOutput(); }); @@ -795,9 +821,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.discreteUpdates(() => { setRow(5); }); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + setRow(20); + }); + } else { setRow(20); - }); + } }); assertLog(['Up', 'Down']); expect(root).toMatchRenderedOutput(); @@ -1309,9 +1339,13 @@ describe('ReactHooksWithNoopRenderer', () => { ]); // Schedule another update for children, and partially process it. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + setChildStates.forEach(setChildState => setChildState(2)); + }); + } else { setChildStates.forEach(setChildState => setChildState(2)); - }); + } await waitFor(['Child one render']); // Schedule unmount for the parent that unmounts children with pending update. @@ -1585,21 +1619,39 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); // Rendering again should flush the previous commit's effects - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(, () => + Scheduler.log('Sync effect'), + ); + }); + } else { ReactNoop.render(, () => Scheduler.log('Sync effect'), ); - }); + } await waitFor(['Schedule update [0]', 'Count: 0']); - expect(ReactNoop).toMatchRenderedOutput(); - await waitFor([ - 'Count: 0', - 'Sync effect', - 'Schedule update [1]', - 'Count: 1', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(ReactNoop).toMatchRenderedOutput(); + await waitFor([ + 'Count: 0', + 'Sync effect', + 'Schedule update [1]', + 'Count: 1', + ]); + } else { + expect(ReactNoop).toMatchRenderedOutput( + , + ); + await waitFor(['Sync effect']); + expect(ReactNoop).toMatchRenderedOutput(); + + ReactNoop.flushPassiveEffects(); + assertLog(['Schedule update [1]']); + await waitForAll(['Count: 1']); + } expect(ReactNoop).toMatchRenderedOutput(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index fbfaba83ff..4a51c73735 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -75,9 +75,13 @@ describe('ReactIncremental', () => { return [, ]; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(, () => Scheduler.log('callback')); + }); + } else { ReactNoop.render(, () => Scheduler.log('callback')); - }); + } // Do one step of work. await waitFor(['Foo']); @@ -164,18 +168,26 @@ describe('ReactIncremental', () => { ReactNoop.render(); await waitForAll(['Foo', 'Bar', 'Bar']); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Flush part of the work await waitFor(['Foo', 'Bar']); // This will abort the previous work and restart ReactNoop.flushSync(() => ReactNoop.render(null)); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Flush part of the new work await waitFor(['Foo', 'Bar']); @@ -209,7 +221,17 @@ describe('ReactIncremental', () => { ReactNoop.render(); await waitForAll([]); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + inst.setState( + () => { + Scheduler.log('setState1'); + return {text: 'bar'}; + }, + () => Scheduler.log('callback1'), + ); + }); + } else { inst.setState( () => { Scheduler.log('setState1'); @@ -217,14 +239,24 @@ describe('ReactIncremental', () => { }, () => Scheduler.log('callback1'), ); - }); + } // Flush part of the work await waitFor(['setState1']); // This will abort the previous work and restart ReactNoop.flushSync(() => ReactNoop.render()); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + inst.setState( + () => { + Scheduler.log('setState2'); + return {text2: 'baz'}; + }, + () => Scheduler.log('callback2'), + ); + }); + } else { inst.setState( () => { Scheduler.log('setState2'); @@ -232,7 +264,7 @@ describe('ReactIncremental', () => { }, () => Scheduler.log('callback2'), ); - }); + } // Flush the rest of the work which now includes the low priority await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']); @@ -1793,7 +1825,18 @@ describe('ReactIncremental', () => { 'ShowLocale {"locale":"de"}', 'ShowBoth {"locale":"de"}', ]); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + + +
+ +
+
, + ); + }); + } else { ReactNoop.render( @@ -1802,7 +1845,7 @@ describe('ReactIncremental', () => { , ); - }); + } await waitFor(['Intl {}']); ReactNoop.render( @@ -1934,7 +1977,22 @@ describe('ReactIncremental', () => { } } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + + + + + + + + + + , + ); + }); + } else { ReactNoop.render( @@ -1947,7 +2005,7 @@ describe('ReactIncremental', () => { , ); - }); + } await waitFor([ 'Intl {}', 'ShowLocale {"locale":"fr"}', @@ -2624,9 +2682,13 @@ describe('ReactIncremental', () => { return null; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['Parent: 1']); // Interrupt at same priority @@ -2646,9 +2708,13 @@ describe('ReactIncremental', () => { return null; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['Parent: 1']); // Interrupt at lower priority @@ -2669,9 +2735,13 @@ describe('ReactIncremental', () => { return null; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['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 2f6b225f2f..ac606f1e68 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -97,7 +97,25 @@ describe('ReactIncrementalErrorHandling', () => { throw new Error('oops!'); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + + + + + + + + + , + ); + }); + } else { ReactNoop.render( <> @@ -113,7 +131,7 @@ describe('ReactIncrementalErrorHandling', () => { , ); - }); + } // Start rendering asynchronously await waitFor([ @@ -196,7 +214,25 @@ describe('ReactIncrementalErrorHandling', () => { throw new Error('oops!'); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + + + + + + + + + , + ); + }); + } else { ReactNoop.render( <> @@ -212,7 +248,7 @@ describe('ReactIncrementalErrorHandling', () => { , ); - }); + } // Start rendering asynchronously await waitFor([ @@ -380,9 +416,13 @@ describe('ReactIncrementalErrorHandling', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(, () => Scheduler.log('commit')); + }); + } else { ReactNoop.render(, () => Scheduler.log('commit')); - }); + } // Render the bad component asynchronously await waitFor(['Parent', 'BadRender']); @@ -418,9 +458,13 @@ describe('ReactIncrementalErrorHandling', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Render part of the tree await waitFor(['A', 'B']); @@ -551,13 +595,21 @@ describe('ReactIncrementalErrorHandling', () => { throw new Error('Hello'); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + + + , + ); + }); + } else { ReactNoop.render( , ); - }); + } await waitFor(['ErrorBoundary render success']); expect(ReactNoop).toMatchRenderedOutput(null); @@ -731,13 +783,21 @@ describe('ReactIncrementalErrorHandling', () => { throw new Error('Hello'); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + + + , + ); + }); + } else { ReactNoop.render( , ); - }); + } await waitFor(['RethrowErrorBoundary render']); @@ -1796,9 +1856,13 @@ describe('ReactIncrementalErrorHandling', () => { } await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } // Render past the component that throws, then yield. await waitFor(['Oops']); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js index cffd690e64..2ae5002b97 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js @@ -65,9 +65,13 @@ describe('ReactIncrementalReflection', () => { return ; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Render part way through but don't yet commit the updates. await waitFor(['componentWillMount: false']); @@ -113,9 +117,13 @@ describe('ReactIncrementalReflection', () => { expect(instances[0]._isMounted()).toBe(true); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Render part way through but don't yet commit the updates so it is not // fully unmounted yet. await waitFor(['Other']); @@ -183,9 +191,13 @@ describe('ReactIncrementalReflection', () => { return [, ]; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Flush past Component but don't complete rendering everything yet. await waitFor([['componentWillMount', null], 'render', 'render sibling']); @@ -215,9 +227,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. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor([ ['componentWillUpdate', hostSpan], 'render', @@ -238,9 +254,13 @@ describe('ReactIncrementalReflection', () => { expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv); // Render to null but don't commit it yet. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor([ ['componentWillUpdate', hostDiv], 'render', diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js index 8c00ec7412..6a1f16fdc2 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js @@ -115,10 +115,15 @@ describe('ReactIncrementalScheduling', () => { // Schedule deferred work in the reverse order await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.renderToRootWithID(, 'c'); + ReactNoop.renderToRootWithID(, 'b'); + }); + } else { ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'b'); - }); + } // Ensure it starts in the order it was scheduled await waitFor(['c:2']); @@ -127,9 +132,13 @@ describe('ReactIncrementalScheduling', () => { expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2'); // Schedule last bit of work, it will get processed the last - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.renderToRootWithID(, 'a'); + }); + } else { ReactNoop.renderToRootWithID(, 'a'); - }); + } // Keep performing work in the order it was scheduled await waitFor(['b:2']); @@ -180,9 +189,13 @@ describe('ReactIncrementalScheduling', () => { } } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Render without committing await waitFor(['render: 0']); @@ -196,9 +209,13 @@ describe('ReactIncrementalScheduling', () => { 'componentDidUpdate: 1', ]); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + instance.setState({tick: 2}); + }); + } else { instance.setState({tick: 2}); - }); + } await waitFor(['render: 2']); expect(ReactNoop.flushNextYield()).toEqual([ 'componentDidUpdate: 2', @@ -299,9 +316,13 @@ describe('ReactIncrementalScheduling', () => { return ; } } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // This should be just enough to complete all the work, but not enough to // commit it. diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js index b47897e54d..5df785b7e0 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js @@ -464,9 +464,13 @@ describe('ReactIncrementalSideEffects', () => { , ); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Flush some of the work without committing await waitFor(['Foo', 'Bar']); @@ -699,9 +703,13 @@ describe('ReactIncrementalSideEffects', () => { Scheduler.log('Foo ' + props.step); return ; } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // This should be just enough to complete the tree without committing it await waitFor(['Foo 1']); expect(ReactNoop.getChildrenAsJSX()).toEqual(null); @@ -710,18 +718,26 @@ describe('ReactIncrementalSideEffects', () => { await waitForPaint([]); expect(ReactNoop.getChildrenAsJSX()).toEqual(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // This should be just enough to complete the tree without committing it await waitFor(['Foo 2']); expect(ReactNoop.getChildrenAsJSX()).toEqual(); // This time, before we commit the tree, we update the root component with // new props - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.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 b224e4d776..13488a7c89 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -156,11 +156,21 @@ describe('ReactIncrementalUpdates', () => { } // Schedule some async updates - React.startTransition(() => { + if ( + gate( + flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane, + ) + ) { + React.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 await waitFor(['a', 'b', 'c']); @@ -177,7 +187,11 @@ describe('ReactIncrementalUpdates', () => { }); // The sync updates should have flushed, but not the async ones. - if (gate(flags => flags.enableUnifiedSyncLane)) { + if ( + gate( + flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane, + ) + ) { assertLog(['d', 'e', 'f']); expect(ReactNoop).toMatchRenderedOutput(); } else { @@ -189,7 +203,11 @@ describe('ReactIncrementalUpdates', () => { // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - if (gate(flags => !flags.enableUnifiedSyncLane)) { + if ( + gate( + flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane, + ) + ) { await waitForAll([ // Since 'g' is in a transition, we'll process 'd' separately first. // That causes us to process 'd' with 'e' and 'f' rebased. @@ -243,11 +261,21 @@ describe('ReactIncrementalUpdates', () => { } // Schedule some async updates - React.startTransition(() => { + if ( + gate( + flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane, + ) + ) { + React.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 await waitFor(['a', 'b', 'c']); @@ -267,7 +295,11 @@ describe('ReactIncrementalUpdates', () => { }); // The sync updates should have flushed, but not the async ones. - if (gate(flags => flags.enableUnifiedSyncLane)) { + if ( + gate( + flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane, + ) + ) { assertLog(['d', 'e', 'f']); } else { // Update d was dropped and replaced by e. @@ -278,7 +310,11 @@ describe('ReactIncrementalUpdates', () => { // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - if (gate(flags => !flags.enableUnifiedSyncLane)) { + if ( + gate( + flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane, + ) + ) { await waitForAll([ // Since 'g' is in a transition, we'll process 'd' separately first. // That causes us to process 'd' with 'e' and 'f' rebased. @@ -507,9 +543,13 @@ describe('ReactIncrementalUpdates', () => { } await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } assertLog([]); await waitForAll([ 'Render: 0', @@ -520,9 +560,13 @@ describe('ReactIncrementalUpdates', () => { ]); Scheduler.unstable_advanceTime(10000); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + setCount(2); + }); + } else { setCount(2); - }); + } // The transition should not have expired, so we should be able to // partially render it. await waitFor(['Render: 2']); @@ -539,7 +583,18 @@ describe('ReactIncrementalUpdates', () => { Scheduler.unstable_advanceTime(10000); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + + , + ); + }); + } else { ReactNoop.render( <> @@ -548,7 +603,7 @@ describe('ReactIncrementalUpdates', () => { , ); - }); + } // The transition should not have expired, so we should be able to // partially render it. await waitFor(['A']); @@ -557,7 +612,18 @@ describe('ReactIncrementalUpdates', () => { }); it('regression: does not expire soon due to previous expired work', async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + + , + ); + }); + } else { ReactNoop.render( <> @@ -566,9 +632,8 @@ describe('ReactIncrementalUpdates', () => { , ); - }); + } await waitFor(['A']); - // This will expire the rest of the update Scheduler.unstable_advanceTime(10000); await waitFor(['B'], { @@ -578,7 +643,18 @@ describe('ReactIncrementalUpdates', () => { Scheduler.unstable_advanceTime(10000); // Now do another transition. This one should not expire. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + + , + ); + }); + } else { ReactNoop.render( <> @@ -587,7 +663,7 @@ describe('ReactIncrementalUpdates', () => { , ); - }); + } // The transition should not have expired, so we should be able to // partially render it. await waitFor(['A']); @@ -627,9 +703,13 @@ describe('ReactIncrementalUpdates', () => { expect(root).toMatchRenderedOutput(null); await act(() => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + pushToLog('A'); + }); + } else { pushToLog('A'); - }); + } ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => pushToLog('B'), @@ -688,9 +768,13 @@ describe('ReactIncrementalUpdates', () => { expect(root).toMatchRenderedOutput(null); await act(() => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.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 27e8652cb8..61e8985ebb 100644 --- a/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js @@ -65,15 +65,78 @@ describe('ReactInterleavedUpdates', () => { expect(root).toMatchRenderedOutput('000'); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + updateChildren(1); + }); + } else { updateChildren(1); - }); + } // Partially render the children. Only the first one. await waitFor([1]); // In an interleaved event, schedule an update on each of the children. // Including the two that haven't rendered yet. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + updateChildren(2); + }); + } else { + updateChildren(2); + } + + // We should continue rendering without including the interleaved updates. + await waitForPaint([1, 1]); + expect(root).toMatchRenderedOutput('111'); + }); + // The interleaved updates flush in a separate render. + assertLog([2, 2, 2]); + expect(root).toMatchRenderedOutput('222'); + }); + + // @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. + const updaters = []; + + function Child() { + const [state, setState] = useState(0); + useEffect(() => { + updaters.push(setState); + }, []); + return ; + } + + function updateChildren(value) { + for (let i = 0; i < updaters.length; i++) { + const setState = updaters[i]; + setState(value); + } + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render( + <> + + + + , + ); + }); + assertLog([0, 0, 0]); + expect(root).toMatchRenderedOutput('000'); + + await act(async () => { + updateChildren(1); + // Partially render the children. Only the first one. + await waitFor([1]); + + // In an interleaved event, schedule an update on each of the children. + // Including the two that haven't rendered yet. + startTransition(() => { updateChildren(2); }); diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index f508ac4abf..0ebf8f2d53 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -1559,9 +1559,13 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.update(); + }); + } else { root.update(); - }); + } await waitForAll(['Init B2', 'Loading...']); await resolveFakeImport(ChildB2); // 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 cedf698148..7f8520088d 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -885,9 +885,13 @@ describe('ReactNewContext', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } // Render past the Provider, but don't commit yet await waitFor(['Foo']); @@ -930,9 +934,13 @@ describe('ReactNewContext', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll(['Foo', 'Foo']); // Get a new copy of ReactNoop diff --git a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js index 17067638ac..f67e254986 100644 --- a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js @@ -109,9 +109,13 @@ describe('ReactSchedulerIntegration', () => { scheduleCallback(NormalPriority, () => Scheduler.log('C')); // Schedule a React render. React will request a paint after committing it. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render('Update'); + }); + } else { root.render('Update'); - }); + } // Perform just a little bit of work. By now, the React task will have // already been scheduled, behind A, B, and C. diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index df51d2b74d..5723e0039e 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -125,9 +125,13 @@ describe('ReactSuspense', () => { // Navigate the shell to now render the child content. // This should suspend. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.update(); + }); + } else { root.update(); - }); + } await waitForAll([ 'Foo', @@ -224,7 +228,19 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Initial'); // The update will suspend. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.update( + <> + }> + + + + + , + ); + }); + } else { root.update( <> }> @@ -234,7 +250,8 @@ describe('ReactSuspense', () => { , ); - }); + } + // Yield past the Suspense boundary but don't complete the last sibling. await waitFor(['Suspend!', 'Loading...', 'After Suspense']); @@ -329,6 +346,76 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('AB'); }); + // @gate !enableSyncDefaultUpdates + it( + 'interrupts current render when something suspends with a ' + + "delay and we've already skipped over a lower priority update in " + + 'a parent', + async () => { + function interrupt() { + // React has a heuristic to batch all updates that occur within the same + // event. This is a trick to circumvent that heuristic. + ReactTestRenderer.create('whatever'); + } + + function App({shouldSuspend, step}) { + return ( + <> + + }> + {shouldSuspend ? : null} + + + + + ); + } + + const root = ReactTestRenderer.create(null, { + unstable_isConcurrent: true, + }); + + root.update(); + await waitForAll(['A0', 'B0', 'C0']); + expect(root).toMatchRenderedOutput('A0B0C0'); + + // This update will suspend. + root.update(); + + // Do a bit of work + await waitFor(['A1']); + + // Schedule another update. This will have lower priority because it's + // a transition. + React.startTransition(() => { + root.update(); + }); + + // Interrupt to trigger a restart. + interrupt(); + + await waitFor([ + // Should have restarted the first update, because of the interruption + 'A1', + 'Suspend! [Async]', + 'Loading...', + 'B1', + ]); + + // Should not have committed loading state + expect(root).toMatchRenderedOutput('A0B0C0'); + + // After suspending, should abort the first update and switch to the + // second update. So, C1 should not appear in the log. + // TODO: This should work even if React does not yield to the main + // thread. Should use same mechanism as selective hydration to interrupt + // the render before the end of the current slice of work. + await waitForAll(['A2', 'B2', 'C2']); + + expect(root).toMatchRenderedOutput('A2B2C2'); + }, + ); + it('mounts a lazy class component in non-concurrent mode', async () => { class Class extends React.Component { componentDidMount() { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js index defd59acf6..a684f6ab4d 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js @@ -576,7 +576,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ]); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be destroyed and recreated for function components', async () => { function App({children = null}) { Scheduler.log('App render'); @@ -711,7 +711,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ]); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be destroyed and recreated for class components', async () => { class ClassText extends React.Component { componentDidMount() { @@ -860,7 +860,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ]); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be destroyed and recreated when nested below host components', async () => { function App({children = null}) { Scheduler.log('App render'); @@ -979,7 +979,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ]); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be destroyed and recreated even if there is a bailout because of memoization', async () => { const MemoizedText = React.memo(Text, () => true); @@ -1448,7 +1448,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be cleaned up inside of a fallback that suspends', async () => { function App({fallbackChildren = null, outerChildren = null}) { return ( @@ -1724,7 +1724,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be cleaned up deeper inside of a subtree that suspends', async () => { function ConditionalSuspense({shouldSuspend}) { if (shouldSuspend) { @@ -2305,7 +2305,7 @@ describe('ReactSuspenseEffectsSemantics', () => { }); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be only destroy layout effects once if a tree suspends in multiple places', async () => { class ClassText extends React.Component { componentDidMount() { @@ -2448,7 +2448,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ]); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && enableSyncDefaultUpdates it('should be only destroy layout effects once if a component suspends multiple times', async () => { class ClassText extends React.Component { componentDidMount() { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index c3da64090e..2b20ca53d8 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -1366,9 +1366,13 @@ describe('ReactSuspenseList', () => { } // This render is only CPU bound. Nothing suspends. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['A']); @@ -1550,9 +1554,13 @@ describe('ReactSuspenseList', () => { } // This render is only CPU bound. Nothing suspends. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['A']); @@ -2517,9 +2525,15 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(null); await act(async () => { - React.startTransition(() => { + // Add a few items at the end. + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + updateLowPri(true); + }); + } else { updateLowPri(true); - }); + } + // Flush partially through. await waitFor(['B', 'C']); @@ -2655,9 +2669,14 @@ describe('ReactSuspenseList', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } + await waitFor(['App', 'First Pass A', 'Mount A', 'A']); expect(ReactNoop).toMatchRenderedOutput(A); @@ -2718,9 +2737,14 @@ describe('ReactSuspenseList', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } + await waitFor([ 'App', 'First Pass A', diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index a6c139daea..9ef6f2630a 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -216,9 +216,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); } - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor([ 'Foo', 'Bar', @@ -285,9 +289,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll(['Foo']); // The update will suspend. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll([ 'Foo', 'Bar', @@ -367,7 +375,18 @@ describe('ReactSuspenseWithNoopRenderer', () => { // A shell is needed. The update cause it to suspend. ReactNoop.render(} />); await waitForAll([]); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + }> + + + + + , + ); + }); + } else { ReactNoop.render( }> @@ -376,7 +395,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); - }); + } // B suspends. Render a fallback await waitForAll(['A', 'Suspend! [B]', 'Loading...']); // Did not commit yet. @@ -434,9 +453,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(null); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll(['Suspend! [Result]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput(null); @@ -581,18 +604,26 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(null); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll(['Suspend! [A]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput(null); // 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); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll(['Suspend! [A]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput(null); @@ -674,23 +705,35 @@ describe('ReactSuspenseWithNoopRenderer', () => { // Schedule an update at several distinct expiration times await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } Scheduler.unstable_advanceTime(1000); await waitFor(['Sibling']); interrupt(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } Scheduler.unstable_advanceTime(1000); await waitFor(['Sibling']); interrupt(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } Scheduler.unstable_advanceTime(1000); await waitFor(['Sibling']); interrupt(); @@ -1004,7 +1047,18 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await waitForAll([]); expect(root).toMatchRenderedOutput(null); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render( + <> + }> + + + + , + ); + }); + } else { root.render( <> }> @@ -1013,7 +1067,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); - }); + } await waitFor(['Suspend! [Async]']); await resolveText('Async'); @@ -1075,13 +1129,21 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(} />); await waitForAll([]); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + }> + + , + ); + }); + } else { ReactNoop.render( }> , ); - }); + } await waitForAll(['Suspend! [Async]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput(null); @@ -1862,9 +1924,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(); await waitForAll(['Foo']); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } Scheduler.unstable_advanceTime(100); await advanceTimers(100); // Start rendering @@ -1893,14 +1959,22 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(500); // No need to rerender. await waitForAll([]); - // Since this is a transition, we never fallback. - expect(ReactNoop).toMatchRenderedOutput(null); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // Since this is a transition, we never fallback. + expect(ReactNoop).toMatchRenderedOutput(null); + } else { + expect(ReactNoop).toMatchRenderedOutput(); + } // Flush the promise completely await resolveText('A'); - await waitForAll(['Foo', 'A']); // Renders successfully - // TODO: Why does this render Foo + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // TODO: Why does this render Foo + await waitForAll(['Foo', 'A']); + } else { + await waitForAll(['A']); + } expect(ReactNoop).toMatchRenderedOutput(); }); @@ -2028,9 +2102,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(); await waitForAll(['Foo']); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitFor(['Foo']); // Advance some time. @@ -2055,8 +2133,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. await waitForAll([]); - // Transitions never fallback. - expect(ReactNoop).toMatchRenderedOutput(null); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // Transitions never fallback. + expect(ReactNoop).toMatchRenderedOutput(null); + } else { + expect(ReactNoop).toMatchRenderedOutput(); + } }); // TODO: flip to "warns" when this is implemented again. @@ -2408,9 +2490,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll(['Foo', 'A']); expect(ReactNoop).toMatchRenderedOutput(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']); // Still suspended. @@ -2420,8 +2506,17 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.unstable_advanceTime(600); await advanceTimers(600); - // Transitions never fall back. - expect(ReactNoop).toMatchRenderedOutput(); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // Transitions never fall back. + expect(ReactNoop).toMatchRenderedOutput(); + } else { + expect(ReactNoop).toMatchRenderedOutput( + <> + + + , + ); + } }); // @gate enableLegacyCache @@ -2446,9 +2541,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll(['Foo', 'A']); expect(ReactNoop).toMatchRenderedOutput(); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + } else { ReactNoop.render(); - }); + } await waitForAll([ 'Foo', @@ -2463,8 +2562,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.unstable_advanceTime(600); await advanceTimers(600); - // Transitions never fall back. - expect(ReactNoop).toMatchRenderedOutput(); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // Transitions never fall back. + expect(ReactNoop).toMatchRenderedOutput(); + } else { + expect(ReactNoop).toMatchRenderedOutput(); + } }); // @gate enableLegacyCache @@ -3000,18 +3103,26 @@ describe('ReactSuspenseWithNoopRenderer', () => { await act(async () => { // Update. Since showing a fallback would hide content that's already // visible, it should suspend for a JND without committing. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend! [First update]']); // Should not display a fallback expect(root).toMatchRenderedOutput(); // Update again. This should also suspend for a JND. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } await waitForAll(['Suspend! [Second update]']); // Should not display a fallback @@ -3775,6 +3886,117 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); + // @gate enableLegacyCache + // @gate !enableSyncDefaultUpdates + it('regression: ping at high priority causes update to be dropped', async () => { + const {useState, useTransition} = React; + + let setTextA; + function A() { + const [textA, _setTextA] = useState('A'); + setTextA = _setTextA; + return ( + }> + + + ); + } + + let setTextB; + let startTransitionFromB; + function B() { + const [textB, _setTextB] = useState('B'); + // eslint-disable-next-line no-unused-vars + const [_, _startTransition] = useTransition(); + startTransitionFromB = _startTransition; + setTextB = _setTextB; + return ( + }> + + + ); + } + + function App() { + return ( + <> + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(async () => { + await seedNextTextCache('A'); + await seedNextTextCache('B'); + root.render(); + }); + assertLog(['A', 'B']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + await act(async () => { + // Triggers suspense at normal pri + setTextA('A1'); + // Triggers in an unrelated tree at a different pri + startTransitionFromB(() => { + // Update A again so that it doesn't suspend on A1. That way we can ping + // the A1 update without also pinging this one. This is a workaround + // because there's currently no way to render at a lower priority (B2) + // without including all updates at higher priority (A1). + setTextA('A2'); + setTextB('B2'); + }); + + await waitFor([ + 'B', + 'Suspend! [A1]', + 'Loading...', + + 'Suspend! [A2]', + 'Loading...', + 'Suspend! [B2]', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + await resolveText('A1'); + await waitFor([ + 'A1', + 'Suspend! [A2]', + 'Loading...', + 'Suspend! [B2]', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + await resolveText('A2'); + await resolveText('B2'); + }); + assertLog(['A2', 'B2']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); + // Regression: https://github.com/facebook/react/issues/18486 // @gate enableLegacyCache it('does not get stuck in pending state with render phase updates', async () => { diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index 9c7365f621..4ae03ac825 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -221,7 +221,27 @@ describe('useMutableSource', () => { const mutableSource = createMutableSource(source, param => param.version); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + , + () => Scheduler.log('Sync effect'), + ); + }); + } else { ReactNoop.render( <> { , () => Scheduler.log('Sync effect'), ); - }); + } // Do enough work to read from one component await waitFor(['a:one']); @@ -436,9 +456,13 @@ describe('useMutableSource', () => { // Changing values should schedule an update with React. // Start working on this update but don't finish it. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + source.value = 'two'; + }); + } else { source.value = 'two'; - }); + } await waitFor(['a:two']); // Re-renders that occur before the update is processed @@ -696,7 +720,33 @@ 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. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + + , + () => Scheduler.log('Sync effect'), + ); + }); + } else { ReactNoop.render( <> { , () => Scheduler.log('Sync effect'), ); - }); + } await waitFor(['a:a:one', 'b:b:one']); // Mutating the source should trigger a tear detection on the next read, @@ -806,7 +856,26 @@ describe('useMutableSource', () => { await act(async () => { // Start a render that uses the mutable source. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + , + ); + }); + } else { ReactNoop.render( <> { /> , ); - }); + } await waitFor(['a:one']); // Mutate source @@ -1455,7 +1524,17 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('a0'); await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render( + <> + + + + , + ); + }); + } else { root.render( <> @@ -1463,7 +1542,7 @@ describe('useMutableSource', () => { , ); - }); + } await waitFor(['a0', 'b0']); // Mutate in an event. This schedules a subscription update on a, which @@ -1597,9 +1676,13 @@ describe('useMutableSource', () => { await act(async () => { // Switch the parent and the child to read using the same config - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.render(); + }); + } else { root.render(); - }); + } // Start rendering the parent, but yield before rendering the child await waitFor(['Parent: 2']); @@ -1610,19 +1693,41 @@ describe('useMutableSource', () => { source.valueB = '3'; }); - // In default sync mode, all of the updates flush sync. - await waitFor([ - // The partial render completes - 'Child: 2', - 'Commit: 2, 2', - 'Parent: 3', - 'Child: 3', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // In default sync mode, all of the updates flush sync. + await waitFor([ + // The partial render completes + 'Child: 2', + 'Commit: 2, 2', + 'Parent: 3', + 'Child: 3', + ]); - await waitForAll([ - // Now finish the rest of the update - 'Commit: 3, 3', - ]); + await waitForAll([ + // Now finish the rest of the update + 'Commit: 3, 3', + ]); + } else { + await waitFor([ + // 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. + // + await waitFor([ + 'Parent: 3', + // Demonstrates that we can yield here + ]); + await waitFor([ + // Now finish the rest of the update + 'Child: 3', + 'Commit: 3, 3', + ]); + } }); }); @@ -1738,7 +1843,26 @@ describe('useMutableSource', () => { await act(async () => { // Start a render that uses the mutable source. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + , + ); + }); + } else { ReactNoop.render( <> { /> , ); - }); + } await waitFor(['a:one']); const PrevScheduler = Scheduler; @@ -1800,7 +1924,26 @@ describe('useMutableSource', () => { await act(async () => { // Start a render that uses the mutable source. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render( + <> + + + , + ); + }); + } else { ReactNoop.render( <> { /> , ); - }); + } await waitFor(['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 f8f77a27f1..87a9d2ea4f 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js +++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js @@ -260,14 +260,23 @@ describe('useMutableSourceHydration', () => { await expect(async () => { await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactDOMClient.hydrateRoot(container, , { + mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.log('Log error: ' + error.message); + }, + }); + }); + } else { ReactDOMClient.hydrateRoot(container, , { mutableSources: [mutableSource], onRecoverableError(error) { Scheduler.log('Log error: ' + error.message); }, }); - }); + } await waitFor(['a:one']); source.value = 'two'; }); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js index 468f62d39a..3e29b28500 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js @@ -95,11 +95,17 @@ describe('ReactTestRendererAsync', () => { } let renderer; - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + }); + } else { renderer = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); - }); + } // Flush the first two siblings await waitFor(['A:1', 'B:1']); @@ -135,11 +141,17 @@ describe('ReactTestRendererAsync', () => { } let renderer; - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + }); + } else { renderer = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); - }); + } // Flush the some of the changes, but don't commit await waitFor(['A:1']); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index de6b7f973e..9ae071f360 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -206,7 +206,19 @@ describe(`onRender`, () => { return null; }; - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactTestRenderer.create( + + + + , + { + unstable_isConcurrent: true, + }, + ); + }); + } else { ReactTestRenderer.create( @@ -216,7 +228,7 @@ describe(`onRender`, () => { unstable_isConcurrent: true, }, ); - }); + } // Times are logged until a render is committed. await waitFor(['first']); @@ -751,7 +763,17 @@ describe(`onRender`, () => { Scheduler.unstable_advanceTime(5); // 0 -> 5 // Render partially, but run out of time before completing. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + }); + } else { ReactTestRenderer.create( @@ -759,7 +781,7 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - }); + } await waitFor(['Yield:2']); expect(callback).toHaveBeenCalledTimes(0); @@ -788,7 +810,20 @@ describe(`onRender`, () => { // Render partially, but don't finish. // This partial render should take 5ms of simulated time. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactTestRenderer.create( + + + + + + + , + {unstable_isConcurrent: true}, + ); + }); + } else { ReactTestRenderer.create( @@ -799,7 +834,7 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - }); + } await waitFor(['Yield:5']); expect(callback).toHaveBeenCalledTimes(0); @@ -841,7 +876,17 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render should take 10ms of simulated time. let renderer; - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + }); + } else { renderer = ReactTestRenderer.create( @@ -849,7 +894,7 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - }); + } await waitFor(['Yield:10']); expect(callback).toHaveBeenCalledTimes(0); @@ -918,7 +963,17 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render should take 3ms of simulated time. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer.update( + + + + + , + ); + }); + } else { renderer.update( @@ -926,7 +981,7 @@ describe(`onRender`, () => { , ); - }); + } await waitFor(['Yield:3']); expect(callback).toHaveBeenCalledTimes(0); @@ -1028,9 +1083,13 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render will take 10ms of actual render time. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + first.setState({renderTime: 10}); + }); + } else { first.setState({renderTime: 10}); - }); + } await waitFor(['FirstComponent:10']); expect(callback).toHaveBeenCalledTimes(0); diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js index d28e2ad8de..218cd3a2da 100644 --- a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js @@ -161,9 +161,13 @@ describe('ReactProfiler DevTools integration', () => { // for updates. Scheduler.unstable_advanceTime(10000); // Schedule an update. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + root.update(); + }); + } else { root.update(); - }); + } // Update B should not instantly expire. await waitFor([]); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 619b4bf152..f3250c17fa 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -142,6 +142,9 @@ export const disableLegacyContext = false; export const enableUseRefAccessWarning = false; +// Enables time slicing for updates that aren't wrapped in startTransition. +export const enableSyncDefaultUpdates = true; + export const enableUnifiedSyncLane = __EXPERIMENTAL__; // Adds an opt-in to time slicing for updates that aren't wrapped in diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3b54b4d9d6..456d78a191 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -64,6 +64,7 @@ export const createRootStrictEffectsByDefault = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableLegacyHidden = true; +export const enableSyncDefaultUpdates = true; export const enableUnifiedSyncLane = false; export const allowConcurrentByDefault = true; export const enableCustomElementPropertySupport = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 0efdd6e8b1..29d464cb88 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableLegacyHidden = false; +export const enableSyncDefaultUpdates = true; export const enableUnifiedSyncLane = false; export const allowConcurrentByDefault = false; export const enableCustomElementPropertySupport = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f4d7962376..f102289afe 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableLegacyHidden = false; +export const enableSyncDefaultUpdates = true; export const enableUnifiedSyncLane = __EXPERIMENTAL__; export const allowConcurrentByDefault = false; export const enableCustomElementPropertySupport = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 4dbf373f8c..e1d66519d5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -53,6 +53,7 @@ export const enableUseRefAccessWarning = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableLegacyHidden = false; +export const enableSyncDefaultUpdates = true; export const enableUnifiedSyncLane = false; export const allowConcurrentByDefault = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index a193740fb0..8e7343adb7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableLegacyHidden = false; +export const enableSyncDefaultUpdates = true; export const enableUnifiedSyncLane = false; export const allowConcurrentByDefault = true; export const enableCustomElementPropertySupport = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 69c5fad893..0926f66fc3 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -20,6 +20,7 @@ export const enableUseRefAccessWarning = __VARIANT__; export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; export const enableLazyContextPropagation = __VARIANT__; +export const enableSyncDefaultUpdates = __VARIANT__; export const enableUnifiedSyncLane = __VARIANT__; export const enableTransitionTracing = __VARIANT__; export const enableCustomElementPropertySupport = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 229b60c37f..f0157f00f7 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -24,6 +24,7 @@ export const { enableDebugTracing, enableUseRefAccessWarning, enableLazyContextPropagation, + enableSyncDefaultUpdates, enableUnifiedSyncLane, enableTransitionTracing, enableCustomElementPropertySupport, diff --git a/packages/use-subscription/src/__tests__/useSubscription-test.js b/packages/use-subscription/src/__tests__/useSubscription-test.js index 19eba0a15b..f3d8d39053 100644 --- a/packages/use-subscription/src/__tests__/useSubscription-test.js +++ b/packages/use-subscription/src/__tests__/useSubscription-test.js @@ -339,9 +339,13 @@ describe('useSubscription', () => { // Start React update, but don't finish await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer.update(); + }); + } else { renderer.update(); - }); + } await waitFor(['Child: b-0']); expect(log).toEqual(['Parent.componentDidMount']); @@ -443,9 +447,13 @@ describe('useSubscription', () => { // Start React update, but don't finish await act(async () => { - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer.update(); + }); + } else { renderer.update(); - }); + } await waitFor(['Child: b-0']); expect(log).toEqual([]); @@ -624,13 +632,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. - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + mutate('C'); + }); + } else { mutate('C'); - }); + } await waitFor(['render:first:C', 'render:second:C']); - React.startTransition(() => { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + mutate('D'); + }); + } else { mutate('D'); - }); + } await waitForAll(['render:first:D', 'render:second:D']); // No more pending updates