diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index 714e19d1cc1f..9f73006d0201 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 60c78035e222..3300dc1bb35c 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 498432186e19..ab09c63a7bcb 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 1a1769e82c07..4b6c5717fa29 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 f06844533e59..1e02b09441a2 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 26a82e8be3e3..2c38a05168a6 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); - - // Now import the renderer. On module initialization, it will read the - // current time. - ReactNoop = require('react-noop-renderer'); - React = require('react'); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + 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); + + // Now import the renderer. On module initialization, it will read the + // current time. + ReactNoop = require('react-noop-renderer'); + React = require('react'); + + ReactNoop.render(); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + ReactNoop.render(); + }); + await waitFor(['Step 1']); + } else { + ReactNoop.render('Hi'); + } - ReactNoop.render(); - React.startTransition(() => { - ReactNoop.render(); - }); - await waitFor(['Step 1']); + // The update should not have expired yet. + await unstable_waitForExpired([]); - // The update should not have expired yet. - await unstable_waitForExpired([]); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); - 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'); - // Advance the time some more to expire the update. - Scheduler.unstable_advanceTime(10000); - await unstable_waitForExpired(['Step 2']); - expect(ReactNoop).toMatchRenderedOutput('Step 2'); + // 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 e2d9ba76660f..57e2aad2e333 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 c62d12075f7b..627dc4e85621 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 fbfaba83ff18..4a51c737350d 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 2f6b225f2f60..ac606f1e6870 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 cffd690e6471..2ae5002b97f7 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 8c00ec741206..6a1f16fdc2c7 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 b47897e54d92..5df785b7e03c 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 b224e4d77648..13488a7c899d 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 27e8652cb8ac..61e8985ebb0a 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. + 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. - React.startTransition(() => { + 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 f508ac4abf5b..0ebf8f2d5329 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 cedf698148ee..7f8520088d64 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 17067638ac81..f67e2549862b 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 df51d2b74d59..5723e0039e2e 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 defd59acf642..a684f6ab4d24 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 c3da64090e25..2b20ca53d8f9 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 a6c139daea7e..9ef6f2630a61 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 9c7365f6216d..4ae03ac8250c 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', - ]); - - await waitForAll([ - // Now finish the rest of the update - 'Commit: 3, 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', + ]); + } 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 f8f77a27f141..87a9d2ea4fb5 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 468f62d39a72..3e29b2850000 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 de6b7f973e66..9ae071f36061 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 d28e2ad8de64..218cd3a2daab 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 619b4bf152a9..f3250c17fa33 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 3b54b4d9d60d..456d78a191e2 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 0efdd6e8b18c..29d464cb8833 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 f4d7962376d4..f102289afedd 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 4dbf373f8c18..e1d66519d50e 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 a193740fb0ab..8e7343adb750 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 69c5fad8938a..0926f66fc3a8 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 229b60c37f15..f0157f00f777 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 19eba0a15be8..f3d8d3905320 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