diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js
index 714e19d1cc1fe..9f73006d0201a 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 60c78035e2223..3300dc1bb35c3 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 498432186e191..ab09c63a7bcb9 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 1a1769e82c07e..4b6c5717fa29b 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 f06844533e59a..1e02b09441a2d 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 26a82e8be3e38..2c38a05168a6c 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 e2d9ba76660f9..57e2aad2e3334 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 c62d12075f7b0..627dc4e856216 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 fbfaba83ff187..4a51c737350d6 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 2f6b225f2f60e..ac606f1e6870a 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 cffd690e64715..2ae5002b97f74 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 8c00ec741206e..6a1f16fdc2c7d 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 b47897e54d92e..5df785b7e03cd 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 b224e4d776482..13488a7c899de 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 27e8652cb8ac4..61e8985ebb0a0 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 f508ac4abf5b9..0ebf8f2d53293 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 cedf698148ee1..7f8520088d64f 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 17067638ac811..f67e2549862bd 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 df51d2b74d59e..5723e0039e2eb 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 defd59acf6423..a684f6ab4d245 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 c3da64090e259..2b20ca53d8f91 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 a6c139daea7e0..9ef6f2630a61c 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 9c7365f6216d9..4ae03ac8250ca 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 f8f77a27f141e..87a9d2ea4fb5f 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 468f62d39a722..3e29b28500003 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 de6b7f973e66b..9ae071f360617 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 d28e2ad8de64a..218cd3a2daabb 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 619b4bf152a9b..f3250c17fa335 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 3b54b4d9d60d1..456d78a191e25 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 0efdd6e8b18c1..29d464cb8833c 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 f4d7962376d46..f102289afedde 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 4dbf373f8c182..e1d66519d50e2 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 a193740fb0ab7..8e7343adb750d 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 69c5fad8938a6..0926f66fc3a80 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 229b60c37f155..f0157f00f777d 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 19eba0a15be8e..f3d8d3905320a 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