Revert "Cleanup enableSyncDefaultUpdate flag (#26236)" (#26528)

This reverts commit b2ae9ddb3b.

While the feature flag is fully rolled out, these tests are also testing
behavior set with an unstable flag on root, which for now we want to
preserve.

Not sure if there's a better way then adding a dynamic feature flag to
the www build?
This commit is contained in:
Jan Kassens 2023-04-04 10:08:14 -04:00 committed by GitHub
parent 0700dd50bd
commit da94e8b24a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1475 additions and 312 deletions

View File

@ -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 (
<CurrentRendererContext.Consumer>
{currentRenderer => {
ops.push(currentRenderer);
return null;
}}
</CurrentRendererContext.Consumer>
);
}
// Using test renderer instead of the DOM renderer here because async
// testing APIs for the DOM renderer don't exist.
ReactNoop.render(
<CurrentRendererContext.Provider value="Test">
<Yield value="A" />
<Yield value="B" />
<LogCurrentRenderer />
<Yield value="C" />
</CurrentRendererContext.Provider>,
);
await waitFor(['A']);
ReactDOM.render(
<Surface>
<LogCurrentRenderer />
<CurrentRendererContext.Provider value="ART">
<LogCurrentRenderer />
</CurrentRendererContext.Provider>
</Surface>,
container,
);
expect(ops).toEqual([null, 'ART']);
ops = [];
await waitFor(['B', 'C']);
expect(ops).toEqual(['Test']);
});
});
describe('ReactARTComponents', () => {

View File

@ -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');
});

View File

@ -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.
});

View File

@ -1984,9 +1984,13 @@ describe('DOMPluginEventSystem', () => {
log.length = 0;
// Increase counter
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Test counter={1} />);
});
} else {
root.render(<Test counter={1} />);
});
}
// Yield before committing
await waitFor(['Test']);

View File

@ -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;
}

View File

@ -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(<Text text="Step 1" />);
React.startTransition(() => {
ReactNoop.render(<Text text="Step 2" />);
});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
ReactNoop.render(<Text text="Step 1" />);
React.startTransition(() => {
ReactNoop.render(<Text text="Step 2" />);
});
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(<span prop="done" />);
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(<span prop="done" />);
}
});
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(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />);
});
}
// 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(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />);
});
}
// 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(<App />);
});
} else {
ReactNoop.render(<App />);
});
}
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(<App />);
});
} else {
root.render(<App />);
});
}
await waitFor(['A']);
await waitFor(['B']);
@ -359,9 +404,13 @@ describe('ReactExpiration', () => {
</>
);
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
});
}
await waitFor(['A']);
await waitFor(['B']);
@ -379,36 +428,62 @@ describe('ReactExpiration', () => {
jest.resetModules();
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
// Before importing the renderer, advance the current time by a number
// larger than the maximum allowed for bitwise operations.
const maxSigned31BitInt = 1073741823;
Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
// Now import the renderer. On module initialization, it will read the
// current time.
ReactNoop = require('react-noop-renderer');
React = require('react');
// Before importing the renderer, advance the current time by a number
// larger than the maximum allowed for bitwise operations.
const maxSigned31BitInt = 1073741823;
Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
ReactNoop.render(<Text text="Step 1" />);
React.startTransition(() => {
ReactNoop.render(<Text text="Step 2" />);
});
await waitFor(['Step 1']);
// Now import the renderer. On module initialization, it will read the
// current time.
ReactNoop = require('react-noop-renderer');
React = require('react');
// The update should not have expired yet.
await unstable_waitForExpired([]);
ReactNoop.render(<Text text="Step 1" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Text text="Step 2" />);
});
await waitFor(['Step 1']);
} else {
ReactNoop.render('Hi');
}
expect(ReactNoop).toMatchRenderedOutput('Step 1');
// The update should not have expired yet.
await unstable_waitForExpired([]);
// Advance the time some more to expire the update.
Scheduler.unstable_advanceTime(10000);
await unstable_waitForExpired(['Step 2']);
expect(ReactNoop).toMatchRenderedOutput('Step 2');
expect(ReactNoop).toMatchRenderedOutput('Step 1');
// Advance the time some more to expire the update.
Scheduler.unstable_advanceTime(10000);
await unstable_waitForExpired(['Step 2']);
expect(ReactNoop).toMatchRenderedOutput('Step 2');
} else {
// Before importing the renderer, advance the current time by a number
// larger than the maximum allowed for bitwise operations.
const maxSigned31BitInt = 1073741823;
Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
// Now import the renderer. On module initialization, it will read the
// current time.
ReactNoop = require('react-noop-renderer');
ReactNoop.render('Hi');
// The update should not have expired yet.
flushNextRenderIfExpired();
await waitFor([]);
expect(ReactNoop).toMatchRenderedOutput(null);
// Advance the time some more to expire the update.
Scheduler.unstable_advanceTime(10000);
flushNextRenderIfExpired();
await waitFor([]);
expect(ReactNoop).toMatchRenderedOutput('Hi');
}
});
it('should measure callback timeout relative to current time, not start-up time', async () => {
@ -419,9 +494,13 @@ describe('ReactExpiration', () => {
// Before scheduling an update, advance the current time.
Scheduler.unstable_advanceTime(10000);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render('Hi');
});
} else {
ReactNoop.render('Hi');
});
}
await unstable_waitForExpired([]);
expect(ReactNoop).toMatchRenderedOutput(null);
@ -462,9 +541,13 @@ describe('ReactExpiration', () => {
// First demonstrate what happens when there's no starvation
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri();
});
}
await waitFor(['Sync pri: 0']);
updateSyncPri();
assertLog(['Sync pri: 1', 'Normal pri: 0']);
@ -482,9 +565,13 @@ describe('ReactExpiration', () => {
// Do the same thing, but starve the first update
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri();
});
}
await waitFor(['Sync pri: 1']);
// This time, a lot of time has elapsed since the normal pri update
@ -645,9 +732,13 @@ describe('ReactExpiration', () => {
expect(root).toMatchRenderedOutput('A0BC');
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App step={1} />);
});
} else {
root.render(<App step={1} />);
});
}
await waitForAll(['Suspend! [A1]', 'Loading...']);
// Lots of time elapses before the promise resolves

View File

@ -49,9 +49,13 @@ describe('ReactFlushSync', () => {
const root = ReactNoop.createRoot();
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App />);
});
} else {
root.render(<App />);
});
}
// This will yield right before the passive effect fires
await waitForPaint(['0, 0']);

View File

@ -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(<span prop={0} />);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
});
}
await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop={0} />);
// Rendering again should suspend again.
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
});
}
await waitForAll(['Suspend!']);
});
@ -742,25 +755,38 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Foo signal={false} />);
setLabel('B');
});
} else {
root.render(<Foo signal={false} />);
setLabel('B');
});
}
await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
// Rendering again should suspend again.
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
} else {
root.render(<Foo signal={false} />);
});
}
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(<Foo signal={true} />);
});
} else {
root.render(<Foo signal={true} />);
});
}
await waitForAll(['B:0']);
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
});
@ -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(<span prop="Down" />);
@ -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(<span prop="Count: (empty)" />);
// Rendering again should flush the previous commit's effects
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Counter count={1} />, () =>
Scheduler.log('Sync effect'),
);
});
} else {
ReactNoop.render(<Counter count={1} />, () =>
Scheduler.log('Sync effect'),
);
});
}
await waitFor(['Schedule update [0]', 'Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
await waitFor([
'Count: 0',
'Sync effect',
'Schedule update [1]',
'Count: 1',
]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
await waitFor([
'Count: 0',
'Sync effect',
'Schedule update [1]',
'Count: 1',
]);
} else {
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Count: (empty)" />,
);
await waitFor(['Sync effect']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
ReactNoop.flushPassiveEffects();
assertLog(['Schedule update [1]']);
await waitForAll(['Count: 1']);
}
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
});

View File

@ -75,9 +75,13 @@ describe('ReactIncremental', () => {
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
});
} else {
ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
});
}
// Do one step of work.
await waitFor(['Foo']);
@ -164,18 +168,26 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo text="foo" />);
await waitForAll(['Foo', 'Bar', 'Bar']);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo text="bar" />);
});
} else {
ReactNoop.render(<Foo text="bar" />);
});
}
// 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(<Foo text="baz" />);
});
} else {
ReactNoop.render(<Foo text="baz" />);
});
}
// Flush part of the new work
await waitFor(['Foo', 'Bar']);
@ -209,7 +221,17 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo />);
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(<Foo />));
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(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
});
} else {
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
@ -1802,7 +1845,7 @@ describe('ReactIncremental', () => {
</div>
</Intl>,
);
});
}
await waitFor(['Intl {}']);
ReactNoop.render(
@ -1934,7 +1977,22 @@ describe('ReactIncremental', () => {
}
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<LegacyHiddenDiv mode="hidden">
<ShowLocale />
<Intl locale="ru">
<ShowLocale />
</Intl>
</LegacyHiddenDiv>
<ShowLocale />
</Intl>,
);
});
} else {
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
@ -1947,7 +2005,7 @@ describe('ReactIncremental', () => {
<ShowLocale />
</Intl>,
);
});
}
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(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
});
}
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(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
});
}
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(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />);
});
}
await waitFor(['Parent: 1']);
// Interrupt at higher priority

View File

@ -97,7 +97,25 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('oops!');
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<>
<ErrorBoundary>
<Indirection>
<Indirection>
<Indirection>
<BadRender />
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>
<Indirection />
<Indirection />
</>,
);
});
} else {
ReactNoop.render(
<>
<ErrorBoundary>
@ -113,7 +131,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Indirection />
</>,
);
});
}
// 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(
<>
<ErrorBoundary>
<Indirection>
<Indirection>
<Indirection>
<BadRender />
</Indirection>
</Indirection>
</Indirection>
</ErrorBoundary>
<Indirection />
<Indirection />
</>,
);
});
} else {
ReactNoop.render(
<>
<ErrorBoundary>
@ -212,7 +248,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Indirection />
</>,
);
});
}
// Start rendering asynchronously
await waitFor([
@ -380,9 +416,13 @@ describe('ReactIncrementalErrorHandling', () => {
);
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Parent />, () => Scheduler.log('commit'));
});
} else {
ReactNoop.render(<Parent />, () => 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(<App />);
});
} else {
ReactNoop.render(<App />);
});
}
// 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(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
} else {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
}
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(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
});
} else {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
});
}
await waitFor(['RethrowErrorBoundary render']);
@ -1796,9 +1856,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Oops />);
});
} else {
root.render(<Oops />);
});
}
// Render past the component that throws, then yield.
await waitFor(['Oops']);

View File

@ -65,9 +65,13 @@ describe('ReactIncrementalReflection', () => {
return <Component />;
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
// 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(<Foo mount={false} />);
});
} else {
ReactNoop.render(<Foo mount={false} />);
});
}
// 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 [<Component key="a" step={props.step} />, <Sibling key="b" />];
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo step={0} />);
});
} else {
ReactNoop.render(<Foo step={0} />);
});
}
// 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(<Foo step={2} />);
});
} else {
ReactNoop.render(<Foo step={2} />);
});
}
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(<Foo step={3} />);
});
} else {
ReactNoop.render(<Foo step={3} />);
});
}
await waitFor([
['componentWillUpdate', hostDiv],
'render',

View File

@ -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(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
});
} else {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, '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(<Text text="a:2" />, 'a');
});
} else {
ReactNoop.renderToRootWithID(<Text text="a:2" />, '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(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
// 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 <span prop={this.state.step} />;
}
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
// This should be just enough to complete all the work, but not enough to
// commit it.

View File

@ -464,9 +464,13 @@ describe('ReactIncrementalSideEffects', () => {
</div>,
);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo text="World" />);
});
} else {
ReactNoop.render(<Foo text="World" />);
});
}
// Flush some of the work without committing
await waitFor(['Foo', 'Bar']);
@ -699,9 +703,13 @@ describe('ReactIncrementalSideEffects', () => {
Scheduler.log('Foo ' + props.step);
return <span prop={props.step} />;
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo step={1} />);
});
} else {
ReactNoop.render(<Foo step={1} />);
});
}
// 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(<span prop={1} />);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo step={2} />);
});
} else {
ReactNoop.render(<Foo step={2} />);
});
}
// This should be just enough to complete the tree without committing it
await waitFor(['Foo 2']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
// 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(<Foo step={3} />);
});
} else {
ReactNoop.render(<Foo step={3} />);
});
}
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
// Now let's commit. We already had a commit that was pending, which will
// render 2.

View File

@ -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(<span prop="def" />);
} 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(<App />);
});
} else {
ReactNoop.render(<App />);
});
}
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(
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
<Text text="D" />
</>,
);
});
} else {
ReactNoop.render(
<>
<Text text="A" />
@ -548,7 +603,7 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" />
</>,
);
});
}
// 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(
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
<Text text="D" />
</>,
);
});
} else {
ReactNoop.render(
<>
<Text text="A" />
@ -566,9 +632,8 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" />
</>,
);
});
}
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(
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
<Text text="D" />
</>,
);
});
} else {
ReactNoop.render(
<>
<Text text="A" />
@ -587,7 +663,7 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" />
</>,
);
});
}
// 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'),
);

View File

@ -65,15 +65,78 @@ describe('ReactInterleavedUpdates', () => {
expect(root).toMatchRenderedOutput('000');
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateChildren(1);
});
} else {
updateChildren(1);
});
}
// Partially render the children. Only the first one.
await waitFor([1]);
// In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet.
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateChildren(2);
});
} else {
updateChildren(2);
}
// We should continue rendering without including the interleaved updates.
await waitForPaint([1, 1]);
expect(root).toMatchRenderedOutput('111');
});
// The interleaved updates flush in a separate render.
assertLog([2, 2, 2]);
expect(root).toMatchRenderedOutput('222');
});
// @gate !enableSyncDefaultUpdates
test('low priority update during an interleaved event is not processed during the current render', async () => {
// Same as previous test, but the interleaved update is lower priority than
// the in-progress render.
const updaters = [];
function Child() {
const [state, setState] = useState(0);
useEffect(() => {
updaters.push(setState);
}, []);
return <Text text={state} />;
}
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(
<>
<Child />
<Child />
<Child />
</>,
);
});
assertLog([0, 0, 0]);
expect(root).toMatchRenderedOutput('000');
await act(async () => {
updateChildren(1);
// Partially render the children. Only the first one.
await waitFor([1]);
// In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet.
startTransition(() => {
updateChildren(2);
});

View File

@ -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(<Parent swap={true} />);
});
} else {
root.update(<Parent swap={true} />);
});
}
await waitForAll(['Init B2', 'Loading...']);
await resolveFakeImport(ChildB2);
// We need to flush to trigger the second one to load.

View File

@ -885,9 +885,13 @@ describe('ReactNewContext', () => {
);
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<App value={1} />);
});
} else {
ReactNoop.render(<App value={1} />);
});
}
// 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(<App value={1} />);
});
} else {
ReactNoop.render(<App value={1} />);
});
}
await waitForAll(['Foo', 'Foo']);
// Get a new copy of ReactNoop

View File

@ -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.

View File

@ -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(<Foo renderBar={true} />);
});
} else {
root.update(<Foo renderBar={true} />);
});
}
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(
<>
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
<Text text="After Suspense" />
<Text text="Sibling" />
</>,
);
});
} else {
root.update(
<>
<Suspense fallback={<Text text="Loading..." />}>
@ -234,7 +250,8 @@ describe('ReactSuspense', () => {
<Text text="Sibling" />
</>,
);
});
}
// 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 (
<>
<Text text={`A${step}`} />
<Suspense fallback={<Text text="Loading..." />}>
{shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
</Suspense>
<Text text={`B${step}`} />
<Text text={`C${step}`} />
</>
);
}
const root = ReactTestRenderer.create(null, {
unstable_isConcurrent: true,
});
root.update(<App shouldSuspend={false} step={0} />);
await waitForAll(['A0', 'B0', 'C0']);
expect(root).toMatchRenderedOutput('A0B0C0');
// This update will suspend.
root.update(<App shouldSuspend={true} step={1} />);
// 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(<App shouldSuspend={false} step={2} />);
});
// 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() {

View File

@ -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() {

View File

@ -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(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
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(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
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(<App />);
});
} else {
ReactNoop.render(<App />);
});
}
await waitFor(['App', 'First Pass A', 'Mount A', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
@ -2718,9 +2737,14 @@ describe('ReactSuspenseList', () => {
);
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />);
});
}
await waitFor([
'App',
'First Pass A',

View File

@ -216,9 +216,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
}
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />);
});
}
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(<Foo renderBar={true} />);
});
} else {
ReactNoop.render(<Foo renderBar={true} />);
});
}
await waitForAll([
'Foo',
'Bar',
@ -367,7 +375,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// A shell is needed. The update cause it to suspend.
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
await waitForAll([]);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
<AsyncText text="B" />
<Text text="C" />
<Text text="D" />
</Suspense>,
);
});
} else {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
@ -376,7 +395,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<Text text="D" />
</Suspense>,
);
});
}
// 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(<App renderContent={true} />);
});
} else {
ReactNoop.render(<App renderContent={true} />);
});
}
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(<App showA={true} showB={false} />);
});
} else {
ReactNoop.render(<App showA={true} showB={false} />);
});
}
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(<App showA={true} showB={true} />);
});
} else {
ReactNoop.render(<App showA={true} showB={true} />);
});
}
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(<App step={1} shouldSuspend={true} />);
});
} else {
root.render(<App step={1} shouldSuspend={true} />);
});
}
Scheduler.unstable_advanceTime(1000);
await waitFor(['Sibling']);
interrupt();
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App step={2} shouldSuspend={true} />);
});
} else {
root.render(<App step={2} shouldSuspend={true} />);
});
}
Scheduler.unstable_advanceTime(1000);
await waitFor(['Sibling']);
interrupt();
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App step={3} shouldSuspend={true} />);
});
} else {
root.render(<App step={3} shouldSuspend={true} />);
});
}
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(
<>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
<Text text="Sibling" />
</Suspense>
</>,
);
});
} else {
root.render(
<>
<Suspense fallback={<Text text="Loading..." />}>
@ -1013,7 +1067,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Suspense>
</>,
);
});
}
await waitFor(['Suspend! [Async]']);
await resolveText('Async');
@ -1075,13 +1129,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
await waitForAll([]);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>,
);
});
} else {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Suspense>,
);
});
}
await waitForAll(['Suspend! [Async]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null);
@ -1862,9 +1924,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
await waitForAll(['Foo']);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />);
});
}
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(<span prop="Loading..." />);
}
// 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(<span prop="A" />);
});
@ -2028,9 +2102,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
await waitForAll(['Foo']);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />);
});
}
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(<span prop="Loading..." />);
}
});
// TODO: flip to "warns" when this is implemented again.
@ -2408,9 +2490,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll(['Foo', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo showB={true} />);
});
} else {
ReactNoop.render(<Foo showB={true} />);
});
}
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(<span prop="A" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
} else {
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="Loading B..." />
</>,
);
}
});
// @gate enableLegacyCache
@ -2446,9 +2541,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll(['Foo', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo showB={true} />);
});
} else {
ReactNoop.render(<Foo showB={true} />);
});
}
await waitForAll([
'Foo',
@ -2463,8 +2562,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(600);
await advanceTimers(600);
// Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
} else {
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
}
});
// @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(<App text="First update" />);
});
} else {
root.render(<App text="First update" />);
});
}
await waitForAll(['Suspend! [First update]']);
// Should not display a fallback
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
// Update again. This should also suspend for a JND.
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App text="Second update" />);
});
} else {
root.render(<App text="Second update" />);
});
}
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 (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={textA} />
</Suspense>
);
}
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 (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={textB} />
</Suspense>
);
}
function App() {
return (
<>
<A />
<B />
</>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
await seedNextTextCache('A');
await seedNextTextCache('B');
root.render(<App />);
});
assertLog(['A', 'B']);
expect(root).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="B" />
</>,
);
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(
<>
<span prop="A" />
<span prop="B" />
</>,
);
await resolveText('A1');
await waitFor([
'A1',
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
]);
expect(root).toMatchRenderedOutput(
<>
<span prop="A1" />
<span prop="B" />
</>,
);
await resolveText('A2');
await resolveText('B2');
});
assertLog(['A2', 'B2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="A2" />
<span prop="B2" />
</>,
);
});
// Regression: https://github.com/facebook/react/issues/18486
// @gate enableLegacyCache
it('does not get stuck in pending state with render phase updates', async () => {

View File

@ -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(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
() => Scheduler.log('Sync effect'),
);
});
} else {
ReactNoop.render(
<>
<Component
@ -239,7 +259,7 @@ describe('useMutableSource', () => {
</>,
() => 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(
<>
<Component
label="a"
getSnapshot={getSnapshotA}
mutableSource={mutableSource}
subscribe={subscribeA}
/>
<Component
label="b"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
<Component
label="c"
getSnapshot={getSnapshotB}
mutableSource={mutableSource}
subscribe={subscribeB}
/>
</>,
() => Scheduler.log('Sync effect'),
);
});
} else {
ReactNoop.render(
<>
<Component
@ -720,7 +770,7 @@ describe('useMutableSource', () => {
</>,
() => 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(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
@ -823,7 +892,7 @@ describe('useMutableSource', () => {
/>
</>,
);
});
}
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(
<>
<Read getSnapshot={getSnapshotA} />
<Read getSnapshot={getSnapshotB} />
<Text text="c" />
</>,
);
});
} else {
root.render(
<>
<Read getSnapshot={getSnapshotA} />
@ -1463,7 +1542,7 @@ describe('useMutableSource', () => {
<Text text="c" />
</>,
);
});
}
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(<App parentConfig={configB} childConfig={configB} />);
});
} else {
root.render(<App parentConfig={configB} childConfig={configB} />);
});
}
// Start rendering the parent, but yield before rendering the child
await waitFor(['Parent: 2']);
@ -1610,19 +1693,41 @@ describe('useMutableSource', () => {
source.valueB = '3';
});
// In default sync mode, all of the updates flush sync.
await waitFor([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
'Parent: 3',
'Child: 3',
]);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// In default sync mode, all of the updates flush sync.
await waitFor([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
'Parent: 3',
'Child: 3',
]);
await waitForAll([
// Now finish the rest of the update
'Commit: 3, 3',
]);
await waitForAll([
// Now finish the rest of the update
'Commit: 3, 3',
]);
} else {
await waitFor([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
]);
// Now there are two pending mutations at different priorities. But they
// both read the same version of the mutable source, so we must render
// them simultaneously.
//
await waitFor([
'Parent: 3',
// Demonstrates that we can yield here
]);
await waitFor([
// Now finish the rest of the update
'Child: 3',
'Commit: 3, 3',
]);
}
});
});
@ -1738,7 +1843,26 @@ describe('useMutableSource', () => {
await act(async () => {
// Start a render that uses the mutable source.
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
@ -1755,7 +1879,7 @@ describe('useMutableSource', () => {
/>
</>,
);
});
}
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(
<>
<Component
label="a"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
<Component
label="b"
getSnapshot={defaultGetSnapshot}
mutableSource={mutableSource}
subscribe={defaultSubscribe}
/>
</>,
);
});
} else {
ReactNoop.render(
<>
<Component
@ -1817,7 +1960,7 @@ describe('useMutableSource', () => {
/>
</>,
);
});
}
await waitFor(['a:one']);
const PrevScheduler = Scheduler;

View File

@ -260,14 +260,23 @@ describe('useMutableSourceHydration', () => {
await expect(async () => {
await act(async () => {
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactDOMClient.hydrateRoot(container, <TestComponent />, {
mutableSources: [mutableSource],
onRecoverableError(error) {
Scheduler.log('Log error: ' + error.message);
},
});
});
} else {
ReactDOMClient.hydrateRoot(container, <TestComponent />, {
mutableSources: [mutableSource],
onRecoverableError(error) {
Scheduler.log('Log error: ' + error.message);
},
});
});
}
await waitFor(['a:one']);
source.value = 'two';
});

View File

@ -95,11 +95,17 @@ describe('ReactTestRendererAsync', () => {
}
let renderer;
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
renderer = ReactTestRenderer.create(<Parent step={1} />, {
unstable_isConcurrent: true,
});
});
} else {
renderer = ReactTestRenderer.create(<Parent step={1} />, {
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(<Example step={1} />, {
unstable_isConcurrent: true,
});
});
} else {
renderer = ReactTestRenderer.create(<Example step={1} />, {
unstable_isConcurrent: true,
});
});
}
// Flush the some of the changes, but don't commit
await waitFor(['A:1']);

View File

@ -206,7 +206,19 @@ describe(`onRender`, () => {
return null;
};
React.startTransition(() => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
<Yield value="last" />
</React.Profiler>,
{
unstable_isConcurrent: true,
},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
@ -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(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
<Yield renderTime={3} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
@ -759,7 +781,7 @@ describe(`onRender`, () => {
</React.Profiler>,
{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(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
<Yield renderTime={10} />
<React.Profiler id="inner" onRender={callback}>
<Yield renderTime={17} />
</React.Profiler>
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
@ -799,7 +834,7 @@ describe(`onRender`, () => {
</React.Profiler>,
{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(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
<Yield renderTime={20} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
} else {
renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
@ -849,7 +894,7 @@ describe(`onRender`, () => {
</React.Profiler>,
{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(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
<Yield renderTime={5} />
<Yield renderTime={9} />
</React.Profiler>,
);
});
} else {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
@ -926,7 +981,7 @@ describe(`onRender`, () => {
<Yield renderTime={9} />
</React.Profiler>,
);
});
}
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);

View File

@ -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(<Text text="B" />);
});
} else {
root.update(<Text text="B" />);
});
}
// Update B should not instantly expire.
await waitFor([]);

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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__;

View File

@ -24,6 +24,7 @@ export const {
enableDebugTracing,
enableUseRefAccessWarning,
enableLazyContextPropagation,
enableSyncDefaultUpdates,
enableUnifiedSyncLane,
enableTransitionTracing,
enableCustomElementPropertySupport,

View File

@ -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(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />);
});
}
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(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />);
});
}
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