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 // Isolate the noop renderer
jest.resetModules(); jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const Scheduler = require('scheduler');
let Group; let Group;
let Shape; let Shape;
let Surface; let Surface;
let TestComponent; let TestComponent;
let waitFor;
const Missing = {}; const Missing = {};
function testDOMNodeStructure(domNode, expectedStructure) { function testDOMNodeStructure(domNode, expectedStructure) {
@ -76,6 +80,8 @@ describe('ReactART', () => {
Shape = ReactART.Shape; Shape = ReactART.Shape;
Surface = ReactART.Surface; Surface = ReactART.Surface;
({waitFor} = require('internal-test-utils'));
TestComponent = class extends React.Component { TestComponent = class extends React.Component {
group = React.createRef(); group = React.createRef();
@ -357,6 +363,58 @@ describe('ReactART', () => {
doClick(instance); doClick(instance);
expect(onClick2).toBeCalled(); 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', () => { describe('ReactARTComponents', () => {

View File

@ -312,7 +312,11 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
expect(container.textContent).toEqual('not hovered'); expect(container.textContent).toEqual('not hovered');
await waitFor(['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'); expect(container.textContent).toEqual('hovered');
}); });

View File

@ -2036,7 +2036,14 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true; suspend = true;
await act(async () => { 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. // This will cause us to skip the second row completely.
}); });

View File

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

View File

@ -33,6 +33,7 @@ import {
enableProfilerTimer, enableProfilerTimer,
enableScopeAPI, enableScopeAPI,
enableLegacyHidden, enableLegacyHidden,
enableSyncDefaultUpdates,
allowConcurrentByDefault, allowConcurrentByDefault,
enableTransitionTracing, enableTransitionTracing,
enableDebugTracing, enableDebugTracing,
@ -458,9 +459,11 @@ export function createHostRootFiber(
mode |= StrictLegacyMode | StrictEffectsMode; mode |= StrictLegacyMode | StrictEffectsMode;
} }
if ( 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. // Only for internal experiments.
allowConcurrentByDefault && (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
concurrentUpdatesByDefaultOverride
) { ) {
mode |= ConcurrentUpdatesByDefaultMode; 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 () => { it('increases priority of updates as time progresses', async () => {
ReactNoop.render(<Text text="Step 1" />); if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => { ReactNoop.render(<Text text="Step 1" />);
ReactNoop.render(<Text text="Step 2" />); 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. // Advance time a bit, but not enough to expire the low pri update.
await unstable_waitForExpired([]); ReactNoop.expire(4500);
expect(ReactNoop).toMatchRenderedOutput('Step 1'); await unstable_waitForExpired([]);
expect(ReactNoop).toMatchRenderedOutput('Step 1');
// Advance time a bit, but not enough to expire the low pri update. // Advance by a little bit more. Now the update should expire and flush.
ReactNoop.expire(4500); ReactNoop.expire(500);
await unstable_waitForExpired([]); await unstable_waitForExpired(['Step 2']);
expect(ReactNoop).toMatchRenderedOutput('Step 1'); 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. // Nothing has expired yet because time hasn't advanced.
ReactNoop.expire(500); flushNextRenderIfExpired();
await unstable_waitForExpired(['Step 2']); expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop).toMatchRenderedOutput('Step 2'); // 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 () => { 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. // First, show what happens for updates in two separate events.
// Schedule an update. // Schedule an update.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />); ReactNoop.render(<TextClass text="A" />);
}); }
// Advance the timer. // Advance the timer.
Scheduler.unstable_advanceTime(2000); Scheduler.unstable_advanceTime(2000);
// Partially flush the first update, then interrupt it. // Partially flush the first update, then interrupt it.
@ -219,9 +248,13 @@ describe('ReactExpiration', () => {
// First, show what happens for updates in two separate events. // First, show what happens for updates in two separate events.
// Schedule an update. // Schedule an update.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<TextClass text="A" />);
});
} else {
ReactNoop.render(<TextClass text="A" />); ReactNoop.render(<TextClass text="A" />);
}); }
// Advance the timer. // Advance the timer.
Scheduler.unstable_advanceTime(2000); Scheduler.unstable_advanceTime(2000);
// Partially flush the first update, then interrupt it. // Partially flush the first update, then interrupt it.
@ -287,9 +320,13 @@ describe('ReactExpiration', () => {
} }
// Initial mount // Initial mount
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />); ReactNoop.render(<App />);
}); }
await waitForAll([ await waitForAll([
'initial [A] [render]', 'initial [A] [render]',
'initial [B] [render]', 'initial [B] [render]',
@ -302,9 +339,13 @@ describe('ReactExpiration', () => {
]); ]);
// Partial update // 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'})); subscribers.forEach(s => s.setState({text: '1'}));
}); }
await waitFor(['1 [A] [render]', '1 [B] [render]']); await waitFor(['1 [A] [render]', '1 [B] [render]']);
// Before the update can finish, update again. Even though no time has // 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 />); root.render(<App />);
}); }
await waitFor(['A']); await waitFor(['A']);
await waitFor(['B']); 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 />); root.render(<App />);
}); }
await waitFor(['A']); await waitFor(['A']);
await waitFor(['B']); await waitFor(['B']);
@ -379,36 +428,62 @@ describe('ReactExpiration', () => {
jest.resetModules(); jest.resetModules();
Scheduler = require('scheduler'); 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 if (gate(flags => flags.enableSyncDefaultUpdates)) {
// larger than the maximum allowed for bitwise operations. const InternalTestUtils = require('internal-test-utils');
const maxSigned31BitInt = 1073741823; waitFor = InternalTestUtils.waitFor;
Scheduler.unstable_advanceTime(maxSigned31BitInt * 100); assertLog = InternalTestUtils.assertLog;
unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
// Now import the renderer. On module initialization, it will read the // Before importing the renderer, advance the current time by a number
// current time. // larger than the maximum allowed for bitwise operations.
ReactNoop = require('react-noop-renderer'); const maxSigned31BitInt = 1073741823;
React = require('react'); Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
ReactNoop.render(<Text text="Step 1" />); // Now import the renderer. On module initialization, it will read the
React.startTransition(() => { // current time.
ReactNoop.render(<Text text="Step 2" />); ReactNoop = require('react-noop-renderer');
}); React = require('react');
await waitFor(['Step 1']);
// The update should not have expired yet. ReactNoop.render(<Text text="Step 1" />);
await unstable_waitForExpired([]); 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. expect(ReactNoop).toMatchRenderedOutput('Step 1');
Scheduler.unstable_advanceTime(10000);
await unstable_waitForExpired(['Step 2']); // Advance the time some more to expire the update.
expect(ReactNoop).toMatchRenderedOutput('Step 2'); 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 () => { 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. // Before scheduling an update, advance the current time.
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render('Hi');
});
} else {
ReactNoop.render('Hi'); ReactNoop.render('Hi');
}); }
await unstable_waitForExpired([]); await unstable_waitForExpired([]);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -462,9 +541,13 @@ describe('ReactExpiration', () => {
// First demonstrate what happens when there's no starvation // First demonstrate what happens when there's no starvation
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri(); updateNormalPri();
}); }
await waitFor(['Sync pri: 0']); await waitFor(['Sync pri: 0']);
updateSyncPri(); updateSyncPri();
assertLog(['Sync pri: 1', 'Normal pri: 0']); assertLog(['Sync pri: 1', 'Normal pri: 0']);
@ -482,9 +565,13 @@ describe('ReactExpiration', () => {
// Do the same thing, but starve the first update // Do the same thing, but starve the first update
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateNormalPri();
});
} else {
updateNormalPri(); updateNormalPri();
}); }
await waitFor(['Sync pri: 1']); await waitFor(['Sync pri: 1']);
// This time, a lot of time has elapsed since the normal pri update // This time, a lot of time has elapsed since the normal pri update
@ -645,9 +732,13 @@ describe('ReactExpiration', () => {
expect(root).toMatchRenderedOutput('A0BC'); expect(root).toMatchRenderedOutput('A0BC');
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<App step={1} />);
});
} else {
root.render(<App step={1} />); root.render(<App step={1} />);
}); }
await waitForAll(['Suspend! [A1]', 'Loading...']); await waitForAll(['Suspend! [A1]', 'Loading...']);
// Lots of time elapses before the promise resolves // Lots of time elapses before the promise resolves

View File

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

View File

@ -179,10 +179,15 @@ describe('ReactHooksWithNoopRenderer', () => {
// Schedule some updates // Schedule some updates
await act(async () => { 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(1);
counter.current.updateCount(count => count + 10); counter.current.updateCount(count => count + 10);
}); }
// Partially flush without committing // Partially flush without committing
await waitFor(['Count: 11']); await waitFor(['Count: 11']);
@ -687,16 +692,24 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll([0]); await waitForAll([0]);
expect(root).toMatchRenderedOutput(<span prop={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} />); root.render(<Foo signal={false} />);
}); }
await waitForAll(['Suspend!']); await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop={0} />); expect(root).toMatchRenderedOutput(<span prop={0} />);
// Rendering again should suspend again. // 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} />); root.render(<Foo signal={false} />);
}); }
await waitForAll(['Suspend!']); await waitForAll(['Suspend!']);
}); });
@ -742,25 +755,38 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(root).toMatchRenderedOutput(<span prop="A:0" />); expect(root).toMatchRenderedOutput(<span prop="A:0" />);
await act(async () => { 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} />); root.render(<Foo signal={false} />);
setLabel('B'); setLabel('B');
}); }
await waitForAll(['Suspend!']); await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop="A:0" />); expect(root).toMatchRenderedOutput(<span prop="A:0" />);
// Rendering again should suspend again. // 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} />); root.render(<Foo signal={false} />);
}); }
await waitForAll(['Suspend!']); await waitForAll(['Suspend!']);
// Flip the signal back to "cancel" the update. However, the update to // Flip the signal back to "cancel" the update. However, the update to
// label should still proceed. It shouldn't have been dropped. // 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} />); root.render(<Foo signal={true} />);
}); }
await waitForAll(['B:0']); await waitForAll(['B:0']);
expect(root).toMatchRenderedOutput(<span prop="B:0" />); expect(root).toMatchRenderedOutput(<span prop="B:0" />);
}); });
@ -795,9 +821,13 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.discreteUpdates(() => { ReactNoop.discreteUpdates(() => {
setRow(5); setRow(5);
}); });
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
setRow(20);
});
} else {
setRow(20); setRow(20);
}); }
}); });
assertLog(['Up', 'Down']); assertLog(['Up', 'Down']);
expect(root).toMatchRenderedOutput(<span prop="Down" />); expect(root).toMatchRenderedOutput(<span prop="Down" />);
@ -1309,9 +1339,13 @@ describe('ReactHooksWithNoopRenderer', () => {
]); ]);
// Schedule another update for children, and partially process it. // 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)); setChildStates.forEach(setChildState => setChildState(2));
}); }
await waitFor(['Child one render']); await waitFor(['Child one render']);
// Schedule unmount for the parent that unmounts children with pending update. // Schedule unmount for the parent that unmounts children with pending update.
@ -1585,21 +1619,39 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
// Rendering again should flush the previous commit's effects // 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} />, () => ReactNoop.render(<Counter count={1} />, () =>
Scheduler.log('Sync effect'), Scheduler.log('Sync effect'),
); );
}); }
await waitFor(['Schedule update [0]', 'Count: 0']); await waitFor(['Schedule update [0]', 'Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />); if (gate(flags => flags.enableSyncDefaultUpdates)) {
await waitFor([ expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
'Count: 0', await waitFor([
'Sync effect', 'Count: 0',
'Schedule update [1]', 'Sync effect',
'Count: 1', '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" />); 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} />]; 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')); ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
}); }
// Do one step of work. // Do one step of work.
await waitFor(['Foo']); await waitFor(['Foo']);
@ -164,18 +168,26 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo text="foo" />); ReactNoop.render(<Foo text="foo" />);
await waitForAll(['Foo', 'Bar', 'Bar']); 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" />); ReactNoop.render(<Foo text="bar" />);
}); }
// Flush part of the work // Flush part of the work
await waitFor(['Foo', 'Bar']); await waitFor(['Foo', 'Bar']);
// This will abort the previous work and restart // This will abort the previous work and restart
ReactNoop.flushSync(() => ReactNoop.render(null)); 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" />); ReactNoop.render(<Foo text="baz" />);
}); }
// Flush part of the new work // Flush part of the new work
await waitFor(['Foo', 'Bar']); await waitFor(['Foo', 'Bar']);
@ -209,7 +221,17 @@ describe('ReactIncremental', () => {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
await waitForAll([]); 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( inst.setState(
() => { () => {
Scheduler.log('setState1'); Scheduler.log('setState1');
@ -217,14 +239,24 @@ describe('ReactIncremental', () => {
}, },
() => Scheduler.log('callback1'), () => Scheduler.log('callback1'),
); );
}); }
// Flush part of the work // Flush part of the work
await waitFor(['setState1']); await waitFor(['setState1']);
// This will abort the previous work and restart // This will abort the previous work and restart
ReactNoop.flushSync(() => ReactNoop.render(<Foo />)); 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( inst.setState(
() => { () => {
Scheduler.log('setState2'); Scheduler.log('setState2');
@ -232,7 +264,7 @@ describe('ReactIncremental', () => {
}, },
() => Scheduler.log('callback2'), () => Scheduler.log('callback2'),
); );
}); }
// Flush the rest of the work which now includes the low priority // Flush the rest of the work which now includes the low priority
await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']); await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']);
@ -1793,7 +1825,18 @@ describe('ReactIncremental', () => {
'ShowLocale {"locale":"de"}', 'ShowLocale {"locale":"de"}',
'ShowBoth {"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( ReactNoop.render(
<Intl locale="sv"> <Intl locale="sv">
<ShowLocale /> <ShowLocale />
@ -1802,7 +1845,7 @@ describe('ReactIncremental', () => {
</div> </div>
</Intl>, </Intl>,
); );
}); }
await waitFor(['Intl {}']); await waitFor(['Intl {}']);
ReactNoop.render( 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( ReactNoop.render(
<Intl locale="fr"> <Intl locale="fr">
<ShowLocale /> <ShowLocale />
@ -1947,7 +2005,7 @@ describe('ReactIncremental', () => {
<ShowLocale /> <ShowLocale />
</Intl>, </Intl>,
); );
}); }
await waitFor([ await waitFor([
'Intl {}', 'Intl {}',
'ShowLocale {"locale":"fr"}', 'ShowLocale {"locale":"fr"}',
@ -2624,9 +2682,13 @@ describe('ReactIncremental', () => {
return null; return null;
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />); ReactNoop.render(<Parent step={1} />);
}); }
await waitFor(['Parent: 1']); await waitFor(['Parent: 1']);
// Interrupt at same priority // Interrupt at same priority
@ -2646,9 +2708,13 @@ describe('ReactIncremental', () => {
return null; return null;
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />); ReactNoop.render(<Parent step={1} />);
}); }
await waitFor(['Parent: 1']); await waitFor(['Parent: 1']);
// Interrupt at lower priority // Interrupt at lower priority
@ -2669,9 +2735,13 @@ describe('ReactIncremental', () => {
return null; return null;
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
} else {
ReactNoop.render(<Parent step={1} />); ReactNoop.render(<Parent step={1} />);
}); }
await waitFor(['Parent: 1']); await waitFor(['Parent: 1']);
// Interrupt at higher priority // Interrupt at higher priority

View File

@ -97,7 +97,25 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('oops!'); 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( ReactNoop.render(
<> <>
<ErrorBoundary> <ErrorBoundary>
@ -113,7 +131,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Indirection /> <Indirection />
</>, </>,
); );
}); }
// Start rendering asynchronously // Start rendering asynchronously
await waitFor([ await waitFor([
@ -196,7 +214,25 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('oops!'); 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( ReactNoop.render(
<> <>
<ErrorBoundary> <ErrorBoundary>
@ -212,7 +248,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Indirection /> <Indirection />
</>, </>,
); );
}); }
// Start rendering asynchronously // Start rendering asynchronously
await waitFor([ 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')); ReactNoop.render(<Parent />, () => Scheduler.log('commit'));
}); }
// Render the bad component asynchronously // Render the bad component asynchronously
await waitFor(['Parent', 'BadRender']); 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 />); ReactNoop.render(<App />);
}); }
// Render part of the tree // Render part of the tree
await waitFor(['A', 'B']); await waitFor(['A', 'B']);
@ -551,13 +595,21 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello'); throw new Error('Hello');
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
} else {
ReactNoop.render( ReactNoop.render(
<ErrorBoundary> <ErrorBoundary>
<BrokenRender /> <BrokenRender />
</ErrorBoundary>, </ErrorBoundary>,
); );
}); }
await waitFor(['ErrorBoundary render success']); await waitFor(['ErrorBoundary render success']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -731,13 +783,21 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello'); throw new Error('Hello');
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>,
);
});
} else {
ReactNoop.render( ReactNoop.render(
<RethrowErrorBoundary> <RethrowErrorBoundary>
<BrokenRender /> <BrokenRender />
</RethrowErrorBoundary>, </RethrowErrorBoundary>,
); );
}); }
await waitFor(['RethrowErrorBoundary render']); await waitFor(['RethrowErrorBoundary render']);
@ -1796,9 +1856,13 @@ describe('ReactIncrementalErrorHandling', () => {
} }
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.render(<Oops />);
});
} else {
root.render(<Oops />); root.render(<Oops />);
}); }
// Render past the component that throws, then yield. // Render past the component that throws, then yield.
await waitFor(['Oops']); await waitFor(['Oops']);

View File

@ -65,9 +65,13 @@ describe('ReactIncrementalReflection', () => {
return <Component />; return <Component />;
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
}); }
// Render part way through but don't yet commit the updates. // Render part way through but don't yet commit the updates.
await waitFor(['componentWillMount: false']); await waitFor(['componentWillMount: false']);
@ -113,9 +117,13 @@ describe('ReactIncrementalReflection', () => {
expect(instances[0]._isMounted()).toBe(true); 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} />); ReactNoop.render(<Foo mount={false} />);
}); }
// Render part way through but don't yet commit the updates so it is not // Render part way through but don't yet commit the updates so it is not
// fully unmounted yet. // fully unmounted yet.
await waitFor(['Other']); await waitFor(['Other']);
@ -183,9 +191,13 @@ describe('ReactIncrementalReflection', () => {
return [<Component key="a" step={props.step} />, <Sibling key="b" />]; 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} />); ReactNoop.render(<Foo step={0} />);
}); }
// Flush past Component but don't complete rendering everything yet. // Flush past Component but don't complete rendering everything yet.
await waitFor([['componentWillMount', null], 'render', 'render sibling']); 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. // The next step will render a new host node but won't get committed yet.
// We expect this to mutate the original Fiber. // 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} />); ReactNoop.render(<Foo step={2} />);
}); }
await waitFor([ await waitFor([
['componentWillUpdate', hostSpan], ['componentWillUpdate', hostSpan],
'render', 'render',
@ -238,9 +254,13 @@ describe('ReactIncrementalReflection', () => {
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv); expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
// Render to null but don't commit it yet. // 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} />); ReactNoop.render(<Foo step={3} />);
}); }
await waitFor([ await waitFor([
['componentWillUpdate', hostDiv], ['componentWillUpdate', hostDiv],
'render', 'render',

View File

@ -115,10 +115,15 @@ describe('ReactIncrementalScheduling', () => {
// Schedule deferred work in the reverse order // Schedule deferred work in the reverse order
await act(async () => { 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="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b'); ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
}); }
// Ensure it starts in the order it was scheduled // Ensure it starts in the order it was scheduled
await waitFor(['c:2']); await waitFor(['c:2']);
@ -127,9 +132,13 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2'); expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
// Schedule last bit of work, it will get processed the last // 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'); ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
}); }
// Keep performing work in the order it was scheduled // Keep performing work in the order it was scheduled
await waitFor(['b:2']); 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 />); ReactNoop.render(<Foo />);
}); }
// Render without committing // Render without committing
await waitFor(['render: 0']); await waitFor(['render: 0']);
@ -196,9 +209,13 @@ describe('ReactIncrementalScheduling', () => {
'componentDidUpdate: 1', 'componentDidUpdate: 1',
]); ]);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
instance.setState({tick: 2});
});
} else {
instance.setState({tick: 2}); instance.setState({tick: 2});
}); }
await waitFor(['render: 2']); await waitFor(['render: 2']);
expect(ReactNoop.flushNextYield()).toEqual([ expect(ReactNoop.flushNextYield()).toEqual([
'componentDidUpdate: 2', 'componentDidUpdate: 2',
@ -299,9 +316,13 @@ describe('ReactIncrementalScheduling', () => {
return <span prop={this.state.step} />; return <span prop={this.state.step} />;
} }
} }
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
}); }
// This should be just enough to complete all the work, but not enough to // This should be just enough to complete all the work, but not enough to
// commit it. // commit it.

View File

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

View File

@ -156,11 +156,21 @@ describe('ReactIncrementalUpdates', () => {
} }
// Schedule some async updates // 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('a'));
instance.setState(createUpdate('b')); instance.setState(createUpdate('b'));
instance.setState(createUpdate('c')); instance.setState(createUpdate('c'));
}); }
// Begin the updates but don't flush them yet // Begin the updates but don't flush them yet
await waitFor(['a', 'b', 'c']); await waitFor(['a', 'b', 'c']);
@ -177,7 +187,11 @@ describe('ReactIncrementalUpdates', () => {
}); });
// The sync updates should have flushed, but not the async ones. // 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']); assertLog(['d', 'e', 'f']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />);
} else { } else {
@ -189,7 +203,11 @@ describe('ReactIncrementalUpdates', () => {
// Now flush the remaining work. Even though e and f were already processed, // Now flush the remaining work. Even though e and f were already processed,
// they should be processed again, to ensure that the terminal state // they should be processed again, to ensure that the terminal state
// is deterministic. // is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) { if (
gate(
flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
)
) {
await waitForAll([ await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first. // Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased. // That causes us to process 'd' with 'e' and 'f' rebased.
@ -243,11 +261,21 @@ describe('ReactIncrementalUpdates', () => {
} }
// Schedule some async updates // 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('a'));
instance.setState(createUpdate('b')); instance.setState(createUpdate('b'));
instance.setState(createUpdate('c')); instance.setState(createUpdate('c'));
}); }
// Begin the updates but don't flush them yet // Begin the updates but don't flush them yet
await waitFor(['a', 'b', 'c']); await waitFor(['a', 'b', 'c']);
@ -267,7 +295,11 @@ describe('ReactIncrementalUpdates', () => {
}); });
// The sync updates should have flushed, but not the async ones. // 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']); assertLog(['d', 'e', 'f']);
} else { } else {
// Update d was dropped and replaced by e. // 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, // Now flush the remaining work. Even though e and f were already processed,
// they should be processed again, to ensure that the terminal state // they should be processed again, to ensure that the terminal state
// is deterministic. // is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) { if (
gate(
flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
)
) {
await waitForAll([ await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first. // Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased. // That causes us to process 'd' with 'e' and 'f' rebased.
@ -507,9 +543,13 @@ describe('ReactIncrementalUpdates', () => {
} }
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<App />);
});
} else {
ReactNoop.render(<App />); ReactNoop.render(<App />);
}); }
assertLog([]); assertLog([]);
await waitForAll([ await waitForAll([
'Render: 0', 'Render: 0',
@ -520,9 +560,13 @@ describe('ReactIncrementalUpdates', () => {
]); ]);
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
setCount(2);
});
} else {
setCount(2); setCount(2);
}); }
// The transition should not have expired, so we should be able to // The transition should not have expired, so we should be able to
// partially render it. // partially render it.
await waitFor(['Render: 2']); await waitFor(['Render: 2']);
@ -539,7 +583,18 @@ describe('ReactIncrementalUpdates', () => {
Scheduler.unstable_advanceTime(10000); 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( ReactNoop.render(
<> <>
<Text text="A" /> <Text text="A" />
@ -548,7 +603,7 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" /> <Text text="D" />
</>, </>,
); );
}); }
// The transition should not have expired, so we should be able to // The transition should not have expired, so we should be able to
// partially render it. // partially render it.
await waitFor(['A']); await waitFor(['A']);
@ -557,7 +612,18 @@ describe('ReactIncrementalUpdates', () => {
}); });
it('regression: does not expire soon due to previous expired work', async () => { 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( ReactNoop.render(
<> <>
<Text text="A" /> <Text text="A" />
@ -566,9 +632,8 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" /> <Text text="D" />
</>, </>,
); );
}); }
await waitFor(['A']); await waitFor(['A']);
// This will expire the rest of the update // This will expire the rest of the update
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
await waitFor(['B'], { await waitFor(['B'], {
@ -578,7 +643,18 @@ describe('ReactIncrementalUpdates', () => {
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
// Now do another transition. This one should not expire. // 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( ReactNoop.render(
<> <>
<Text text="A" /> <Text text="A" />
@ -587,7 +663,7 @@ describe('ReactIncrementalUpdates', () => {
<Text text="D" /> <Text text="D" />
</>, </>,
); );
}); }
// The transition should not have expired, so we should be able to // The transition should not have expired, so we should be able to
// partially render it. // partially render it.
await waitFor(['A']); await waitFor(['A']);
@ -627,9 +703,13 @@ describe('ReactIncrementalUpdates', () => {
expect(root).toMatchRenderedOutput(null); expect(root).toMatchRenderedOutput(null);
await act(() => { await act(() => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
pushToLog('A');
});
} else {
pushToLog('A'); pushToLog('A');
}); }
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
pushToLog('B'), pushToLog('B'),
@ -688,9 +768,13 @@ describe('ReactIncrementalUpdates', () => {
expect(root).toMatchRenderedOutput(null); expect(root).toMatchRenderedOutput(null);
await act(() => { await act(() => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
pushToLog('A');
});
} else {
pushToLog('A'); pushToLog('A');
}); }
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
pushToLog('B'), pushToLog('B'),
); );

View File

@ -65,15 +65,78 @@ describe('ReactInterleavedUpdates', () => {
expect(root).toMatchRenderedOutput('000'); expect(root).toMatchRenderedOutput('000');
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateChildren(1);
});
} else {
updateChildren(1); updateChildren(1);
}); }
// Partially render the children. Only the first one. // Partially render the children. Only the first one.
await waitFor([1]); await waitFor([1]);
// In an interleaved event, schedule an update on each of the children. // In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet. // 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); updateChildren(2);
}); });

View File

@ -1559,9 +1559,13 @@ describe('ReactLazy', () => {
expect(root).toMatchRenderedOutput('AB'); expect(root).toMatchRenderedOutput('AB');
// Swap the position of A and B // 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} />); root.update(<Parent swap={true} />);
}); }
await waitForAll(['Init B2', 'Loading...']); await waitForAll(['Init B2', 'Loading...']);
await resolveFakeImport(ChildB2); await resolveFakeImport(ChildB2);
// We need to flush to trigger the second one to load. // 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} />); ReactNoop.render(<App value={1} />);
}); }
// Render past the Provider, but don't commit yet // Render past the Provider, but don't commit yet
await waitFor(['Foo']); 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} />); ReactNoop.render(<App value={1} />);
}); }
await waitForAll(['Foo', 'Foo']); await waitForAll(['Foo', 'Foo']);
// Get a new copy of ReactNoop // Get a new copy of ReactNoop

View File

@ -109,9 +109,13 @@ describe('ReactSchedulerIntegration', () => {
scheduleCallback(NormalPriority, () => Scheduler.log('C')); scheduleCallback(NormalPriority, () => Scheduler.log('C'));
// Schedule a React render. React will request a paint after committing it. // 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'); root.render('Update');
}); }
// Perform just a little bit of work. By now, the React task will have // Perform just a little bit of work. By now, the React task will have
// already been scheduled, behind A, B, and C. // 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. // Navigate the shell to now render the child content.
// This should suspend. // This should suspend.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.update(<Foo renderBar={true} />);
});
} else {
root.update(<Foo renderBar={true} />); root.update(<Foo renderBar={true} />);
}); }
await waitForAll([ await waitForAll([
'Foo', 'Foo',
@ -224,7 +228,19 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Initial'); expect(root).toMatchRenderedOutput('Initial');
// The update will suspend. // 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( root.update(
<> <>
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
@ -234,7 +250,8 @@ describe('ReactSuspense', () => {
<Text text="Sibling" /> <Text text="Sibling" />
</>, </>,
); );
}); }
// Yield past the Suspense boundary but don't complete the last sibling. // Yield past the Suspense boundary but don't complete the last sibling.
await waitFor(['Suspend!', 'Loading...', 'After Suspense']); await waitFor(['Suspend!', 'Loading...', 'After Suspense']);
@ -329,6 +346,76 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AB'); 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 () => { it('mounts a lazy class component in non-concurrent mode', async () => {
class Class extends React.Component { class Class extends React.Component {
componentDidMount() { componentDidMount() {

View File

@ -576,7 +576,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
]); ]);
}); });
// @gate enableLegacyCache // @gate enableLegacyCache && enableSyncDefaultUpdates
it('should be destroyed and recreated for function components', async () => { it('should be destroyed and recreated for function components', async () => {
function App({children = null}) { function App({children = null}) {
Scheduler.log('App render'); 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 () => { it('should be destroyed and recreated for class components', async () => {
class ClassText extends React.Component { class ClassText extends React.Component {
componentDidMount() { componentDidMount() {
@ -860,7 +860,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
]); ]);
}); });
// @gate enableLegacyCache // @gate enableLegacyCache && enableSyncDefaultUpdates
it('should be destroyed and recreated when nested below host components', async () => { it('should be destroyed and recreated when nested below host components', async () => {
function App({children = null}) { function App({children = null}) {
Scheduler.log('App render'); 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 () => { it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
const MemoizedText = React.memo(Text, () => true); 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 () => { it('should be cleaned up inside of a fallback that suspends', async () => {
function App({fallbackChildren = null, outerChildren = null}) { function App({fallbackChildren = null, outerChildren = null}) {
return ( 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 () => { it('should be cleaned up deeper inside of a subtree that suspends', async () => {
function ConditionalSuspense({shouldSuspend}) { function ConditionalSuspense({shouldSuspend}) {
if (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 () => { it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
class ClassText extends React.Component { class ClassText extends React.Component {
componentDidMount() { 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 () => { it('should be only destroy layout effects once if a component suspends multiple times', async () => {
class ClassText extends React.Component { class ClassText extends React.Component {
componentDidMount() { componentDidMount() {

View File

@ -1366,9 +1366,13 @@ describe('ReactSuspenseList', () => {
} }
// This render is only CPU bound. Nothing suspends. // This render is only CPU bound. Nothing suspends.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
}); }
await waitFor(['A']); await waitFor(['A']);
@ -1550,9 +1554,13 @@ describe('ReactSuspenseList', () => {
} }
// This render is only CPU bound. Nothing suspends. // This render is only CPU bound. Nothing suspends.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
} else {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
}); }
await waitFor(['A']); await waitFor(['A']);
@ -2517,9 +2525,15 @@ describe('ReactSuspenseList', () => {
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
await act(async () => { await act(async () => {
React.startTransition(() => { // Add a few items at the end.
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
updateLowPri(true);
});
} else {
updateLowPri(true); updateLowPri(true);
}); }
// Flush partially through. // Flush partially through.
await waitFor(['B', 'C']); 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 />); ReactNoop.render(<App />);
}); }
await waitFor(['App', 'First Pass A', 'Mount A', 'A']); await waitFor(['App', 'First Pass A', 'Mount A', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>); 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 />); ReactNoop.render(<App />);
}); }
await waitFor([ await waitFor([
'App', 'App',
'First Pass A', '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 />); ReactNoop.render(<Foo />);
}); }
await waitFor([ await waitFor([
'Foo', 'Foo',
'Bar', 'Bar',
@ -285,9 +289,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll(['Foo']); await waitForAll(['Foo']);
// The update will suspend. // The update will suspend.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo renderBar={true} />);
});
} else {
ReactNoop.render(<Foo renderBar={true} />); ReactNoop.render(<Foo renderBar={true} />);
}); }
await waitForAll([ await waitForAll([
'Foo', 'Foo',
'Bar', 'Bar',
@ -367,7 +375,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// A shell is needed. The update cause it to suspend. // A shell is needed. The update cause it to suspend.
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />); ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
await waitForAll([]); 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( ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
<Text text="A" /> <Text text="A" />
@ -376,7 +395,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<Text text="D" /> <Text text="D" />
</Suspense>, </Suspense>,
); );
}); }
// B suspends. Render a fallback // B suspends. Render a fallback
await waitForAll(['A', 'Suspend! [B]', 'Loading...']); await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
// Did not commit yet. // Did not commit yet.
@ -434,9 +453,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll([]); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<App renderContent={true} />);
});
} else {
ReactNoop.render(<App renderContent={true} />); ReactNoop.render(<App renderContent={true} />);
}); }
await waitForAll(['Suspend! [Result]', 'Loading...']); await waitForAll(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -581,18 +604,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll([]); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null); 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} />); ReactNoop.render(<App showA={true} showB={false} />);
}); }
await waitForAll(['Suspend! [A]', 'Loading...']); await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
// Advance React's virtual time by enough to fall into a new async bucket, // Advance React's virtual time by enough to fall into a new async bucket,
// but not enough to expire the suspense timeout. // but not enough to expire the suspense timeout.
ReactNoop.expire(120); 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} />); ReactNoop.render(<App showA={true} showB={true} />);
}); }
await waitForAll(['Suspend! [A]', 'Loading...']); await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -674,23 +705,35 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update at several distinct expiration times // Schedule an update at several distinct expiration times
await act(async () => { 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} />); root.render(<App step={1} shouldSuspend={true} />);
}); }
Scheduler.unstable_advanceTime(1000); Scheduler.unstable_advanceTime(1000);
await waitFor(['Sibling']); await waitFor(['Sibling']);
interrupt(); 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} />); root.render(<App step={2} shouldSuspend={true} />);
}); }
Scheduler.unstable_advanceTime(1000); Scheduler.unstable_advanceTime(1000);
await waitFor(['Sibling']); await waitFor(['Sibling']);
interrupt(); 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} />); root.render(<App step={3} shouldSuspend={true} />);
}); }
Scheduler.unstable_advanceTime(1000); Scheduler.unstable_advanceTime(1000);
await waitFor(['Sibling']); await waitFor(['Sibling']);
interrupt(); interrupt();
@ -1004,7 +1047,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
); );
await waitForAll([]); await waitForAll([]);
expect(root).toMatchRenderedOutput(null); 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( root.render(
<> <>
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
@ -1013,7 +1067,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Suspense> </Suspense>
</>, </>,
); );
}); }
await waitFor(['Suspend! [Async]']); await waitFor(['Suspend! [Async]']);
await resolveText('Async'); await resolveText('Async');
@ -1075,13 +1129,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />); ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
await waitForAll([]); 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( ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" /> <AsyncText text="Async" />
</Suspense>, </Suspense>,
); );
}); }
await waitForAll(['Suspend! [Async]', 'Loading...']); await waitForAll(['Suspend! [Async]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -1862,9 +1924,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
await waitForAll(['Foo']); await waitForAll(['Foo']);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />); ReactNoop.render(<Foo renderContent={true} />);
}); }
Scheduler.unstable_advanceTime(100); Scheduler.unstable_advanceTime(100);
await advanceTimers(100); await advanceTimers(100);
// Start rendering // Start rendering
@ -1893,14 +1959,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await advanceTimers(500); await advanceTimers(500);
// No need to rerender. // No need to rerender.
await waitForAll([]); await waitForAll([]);
// Since this is a transition, we never fallback. if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop).toMatchRenderedOutput(null); // Since this is a transition, we never fallback.
expect(ReactNoop).toMatchRenderedOutput(null);
} else {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
}
// Flush the promise completely // Flush the promise completely
await resolveText('A'); await resolveText('A');
await waitForAll(['Foo', 'A']);
// Renders successfully // 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" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
}); });
@ -2028,9 +2102,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
await waitForAll(['Foo']); await waitForAll(['Foo']);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
ReactNoop.render(<Foo renderContent={true} />);
});
} else {
ReactNoop.render(<Foo renderContent={true} />); ReactNoop.render(<Foo renderContent={true} />);
}); }
await waitFor(['Foo']); await waitFor(['Foo']);
// Advance some time. // Advance some time.
@ -2055,8 +2133,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// updates as way earlier in the past. This test ensures that we don't // updates as way earlier in the past. This test ensures that we don't
// use this assumption to add a very long JND. // use this assumption to add a very long JND.
await waitForAll([]); await waitForAll([]);
// Transitions never fallback. if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop).toMatchRenderedOutput(null); // Transitions never fallback.
expect(ReactNoop).toMatchRenderedOutput(null);
} else {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
}
}); });
// TODO: flip to "warns" when this is implemented again. // TODO: flip to "warns" when this is implemented again.
@ -2408,9 +2490,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll(['Foo', 'A']); await waitForAll(['Foo', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="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} />); ReactNoop.render(<Foo showB={true} />);
}); }
await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']); await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
// Still suspended. // Still suspended.
@ -2420,8 +2506,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(600); Scheduler.unstable_advanceTime(600);
await advanceTimers(600); await advanceTimers(600);
// Transitions never fall back. if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />); // Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
} else {
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="Loading B..." />
</>,
);
}
}); });
// @gate enableLegacyCache // @gate enableLegacyCache
@ -2446,9 +2541,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll(['Foo', 'A']); await waitForAll(['Foo', 'A']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="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} />); ReactNoop.render(<Foo showB={true} />);
}); }
await waitForAll([ await waitForAll([
'Foo', 'Foo',
@ -2463,8 +2562,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(600); Scheduler.unstable_advanceTime(600);
await advanceTimers(600); await advanceTimers(600);
// Transitions never fall back. if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />); // Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
} else {
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
}
}); });
// @gate enableLegacyCache // @gate enableLegacyCache
@ -3000,18 +3103,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => { await act(async () => {
// Update. Since showing a fallback would hide content that's already // Update. Since showing a fallback would hide content that's already
// visible, it should suspend for a JND without committing. // 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" />); root.render(<App text="First update" />);
}); }
await waitForAll(['Suspend! [First update]']); await waitForAll(['Suspend! [First update]']);
// Should not display a fallback // Should not display a fallback
expect(root).toMatchRenderedOutput(<span prop="Initial" />); expect(root).toMatchRenderedOutput(<span prop="Initial" />);
// Update again. This should also suspend for a JND. // 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" />); root.render(<App text="Second update" />);
}); }
await waitForAll(['Suspend! [Second update]']); await waitForAll(['Suspend! [Second update]']);
// Should not display a fallback // 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 // Regression: https://github.com/facebook/react/issues/18486
// @gate enableLegacyCache // @gate enableLegacyCache
it('does not get stuck in pending state with render phase updates', async () => { 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); const mutableSource = createMutableSource(source, param => param.version);
await act(async () => { 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( ReactNoop.render(
<> <>
<Component <Component
@ -239,7 +259,7 @@ describe('useMutableSource', () => {
</>, </>,
() => Scheduler.log('Sync effect'), () => Scheduler.log('Sync effect'),
); );
}); }
// Do enough work to read from one component // Do enough work to read from one component
await waitFor(['a:one']); await waitFor(['a:one']);
@ -436,9 +456,13 @@ describe('useMutableSource', () => {
// Changing values should schedule an update with React. // Changing values should schedule an update with React.
// Start working on this update but don't finish it. // 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'; source.value = 'two';
}); }
await waitFor(['a:two']); await waitFor(['a:two']);
// Re-renders that occur before the update is processed // 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, // 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. // 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( ReactNoop.render(
<> <>
<Component <Component
@ -720,7 +770,7 @@ describe('useMutableSource', () => {
</>, </>,
() => Scheduler.log('Sync effect'), () => Scheduler.log('Sync effect'),
); );
}); }
await waitFor(['a:a:one', 'b:b:one']); await waitFor(['a:a:one', 'b:b:one']);
// Mutating the source should trigger a tear detection on the next read, // Mutating the source should trigger a tear detection on the next read,
@ -806,7 +856,26 @@ describe('useMutableSource', () => {
await act(async () => { await act(async () => {
// Start a render that uses the mutable source. // 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( ReactNoop.render(
<> <>
<Component <Component
@ -823,7 +892,7 @@ describe('useMutableSource', () => {
/> />
</>, </>,
); );
}); }
await waitFor(['a:one']); await waitFor(['a:one']);
// Mutate source // Mutate source
@ -1455,7 +1524,17 @@ describe('useMutableSource', () => {
expect(root).toMatchRenderedOutput('a0'); expect(root).toMatchRenderedOutput('a0');
await act(async () => { 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( root.render(
<> <>
<Read getSnapshot={getSnapshotA} /> <Read getSnapshot={getSnapshotA} />
@ -1463,7 +1542,7 @@ describe('useMutableSource', () => {
<Text text="c" /> <Text text="c" />
</>, </>,
); );
}); }
await waitFor(['a0', 'b0']); await waitFor(['a0', 'b0']);
// Mutate in an event. This schedules a subscription update on a, which // Mutate in an event. This schedules a subscription update on a, which
@ -1597,9 +1676,13 @@ describe('useMutableSource', () => {
await act(async () => { await act(async () => {
// Switch the parent and the child to read using the same config // 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} />); root.render(<App parentConfig={configB} childConfig={configB} />);
}); }
// Start rendering the parent, but yield before rendering the child // Start rendering the parent, but yield before rendering the child
await waitFor(['Parent: 2']); await waitFor(['Parent: 2']);
@ -1610,19 +1693,41 @@ describe('useMutableSource', () => {
source.valueB = '3'; source.valueB = '3';
}); });
// In default sync mode, all of the updates flush sync. if (gate(flags => flags.enableSyncDefaultUpdates)) {
await waitFor([ // In default sync mode, all of the updates flush sync.
// The partial render completes await waitFor([
'Child: 2', // The partial render completes
'Commit: 2, 2', 'Child: 2',
'Parent: 3', 'Commit: 2, 2',
'Child: 3', 'Parent: 3',
]); 'Child: 3',
]);
await waitForAll([ await waitForAll([
// Now finish the rest of the update // Now finish the rest of the update
'Commit: 3, 3', '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 () => { await act(async () => {
// Start a render that uses the mutable source. // 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( ReactNoop.render(
<> <>
<Component <Component
@ -1755,7 +1879,7 @@ describe('useMutableSource', () => {
/> />
</>, </>,
); );
}); }
await waitFor(['a:one']); await waitFor(['a:one']);
const PrevScheduler = Scheduler; const PrevScheduler = Scheduler;
@ -1800,7 +1924,26 @@ describe('useMutableSource', () => {
await act(async () => { await act(async () => {
// Start a render that uses the mutable source. // 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( ReactNoop.render(
<> <>
<Component <Component
@ -1817,7 +1960,7 @@ describe('useMutableSource', () => {
/> />
</>, </>,
); );
}); }
await waitFor(['a:one']); await waitFor(['a:one']);
const PrevScheduler = Scheduler; const PrevScheduler = Scheduler;

View File

@ -260,14 +260,23 @@ describe('useMutableSourceHydration', () => {
await expect(async () => { await expect(async () => {
await act(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 />, { ReactDOMClient.hydrateRoot(container, <TestComponent />, {
mutableSources: [mutableSource], mutableSources: [mutableSource],
onRecoverableError(error) { onRecoverableError(error) {
Scheduler.log('Log error: ' + error.message); Scheduler.log('Log error: ' + error.message);
}, },
}); });
}); }
await waitFor(['a:one']); await waitFor(['a:one']);
source.value = 'two'; source.value = 'two';
}); });

View File

@ -95,11 +95,17 @@ describe('ReactTestRendererAsync', () => {
} }
let renderer; 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} />, { renderer = ReactTestRenderer.create(<Parent step={1} />, {
unstable_isConcurrent: true, unstable_isConcurrent: true,
}); });
}); }
// Flush the first two siblings // Flush the first two siblings
await waitFor(['A:1', 'B:1']); await waitFor(['A:1', 'B:1']);
@ -135,11 +141,17 @@ describe('ReactTestRendererAsync', () => {
} }
let renderer; 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} />, { renderer = ReactTestRenderer.create(<Example step={1} />, {
unstable_isConcurrent: true, unstable_isConcurrent: true,
}); });
}); }
// Flush the some of the changes, but don't commit // Flush the some of the changes, but don't commit
await waitFor(['A:1']); await waitFor(['A:1']);

View File

@ -206,7 +206,19 @@ describe(`onRender`, () => {
return null; 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( ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}> <React.Profiler id="test" onRender={callback}>
<Yield value="first" /> <Yield value="first" />
@ -216,7 +228,7 @@ describe(`onRender`, () => {
unstable_isConcurrent: true, unstable_isConcurrent: true,
}, },
); );
}); }
// Times are logged until a render is committed. // Times are logged until a render is committed.
await waitFor(['first']); await waitFor(['first']);
@ -751,7 +763,17 @@ describe(`onRender`, () => {
Scheduler.unstable_advanceTime(5); // 0 -> 5 Scheduler.unstable_advanceTime(5); // 0 -> 5
// Render partially, but run out of time before completing. // 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( ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}> <React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} /> <Yield renderTime={2} />
@ -759,7 +781,7 @@ describe(`onRender`, () => {
</React.Profiler>, </React.Profiler>,
{unstable_isConcurrent: true}, {unstable_isConcurrent: true},
); );
}); }
await waitFor(['Yield:2']); await waitFor(['Yield:2']);
expect(callback).toHaveBeenCalledTimes(0); expect(callback).toHaveBeenCalledTimes(0);
@ -788,7 +810,20 @@ describe(`onRender`, () => {
// Render partially, but don't finish. // Render partially, but don't finish.
// This partial render should take 5ms of simulated time. // 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( ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}> <React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} /> <Yield renderTime={5} />
@ -799,7 +834,7 @@ describe(`onRender`, () => {
</React.Profiler>, </React.Profiler>,
{unstable_isConcurrent: true}, {unstable_isConcurrent: true},
); );
}); }
await waitFor(['Yield:5']); await waitFor(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0); expect(callback).toHaveBeenCalledTimes(0);
@ -841,7 +876,17 @@ describe(`onRender`, () => {
// Render a partially update, but don't finish. // Render a partially update, but don't finish.
// This partial render should take 10ms of simulated time. // This partial render should take 10ms of simulated time.
let renderer; 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( renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}> <React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} /> <Yield renderTime={10} />
@ -849,7 +894,7 @@ describe(`onRender`, () => {
</React.Profiler>, </React.Profiler>,
{unstable_isConcurrent: true}, {unstable_isConcurrent: true},
); );
}); }
await waitFor(['Yield:10']); await waitFor(['Yield:10']);
expect(callback).toHaveBeenCalledTimes(0); expect(callback).toHaveBeenCalledTimes(0);
@ -918,7 +963,17 @@ describe(`onRender`, () => {
// Render a partially update, but don't finish. // Render a partially update, but don't finish.
// This partial render should take 3ms of simulated time. // 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( renderer.update(
<React.Profiler id="test" onRender={callback}> <React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} /> <Yield renderTime={3} />
@ -926,7 +981,7 @@ describe(`onRender`, () => {
<Yield renderTime={9} /> <Yield renderTime={9} />
</React.Profiler>, </React.Profiler>,
); );
}); }
await waitFor(['Yield:3']); await waitFor(['Yield:3']);
expect(callback).toHaveBeenCalledTimes(0); expect(callback).toHaveBeenCalledTimes(0);
@ -1028,9 +1083,13 @@ describe(`onRender`, () => {
// Render a partially update, but don't finish. // Render a partially update, but don't finish.
// This partial render will take 10ms of actual render time. // 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}); first.setState({renderTime: 10});
}); }
await waitFor(['FirstComponent:10']); await waitFor(['FirstComponent:10']);
expect(callback).toHaveBeenCalledTimes(0); expect(callback).toHaveBeenCalledTimes(0);

View File

@ -161,9 +161,13 @@ describe('ReactProfiler DevTools integration', () => {
// for updates. // for updates.
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
// Schedule an update. // Schedule an update.
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
root.update(<Text text="B" />);
});
} else {
root.update(<Text text="B" />); root.update(<Text text="B" />);
}); }
// Update B should not instantly expire. // Update B should not instantly expire.
await waitFor([]); await waitFor([]);

View File

@ -142,6 +142,9 @@ export const disableLegacyContext = false;
export const enableUseRefAccessWarning = false; export const enableUseRefAccessWarning = false;
// Enables time slicing for updates that aren't wrapped in startTransition.
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = __EXPERIMENTAL__; export const enableUnifiedSyncLane = __EXPERIMENTAL__;
// Adds an opt-in to time slicing for updates that aren't wrapped in // 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 disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false; export const enableLazyContextPropagation = false;
export const enableLegacyHidden = true; export const enableLegacyHidden = true;
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = false; export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true; export const allowConcurrentByDefault = true;
export const enableCustomElementPropertySupport = false; export const enableCustomElementPropertySupport = false;

View File

@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
export const disableSchedulerTimeoutInWorkLoop = false; export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false; export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false; export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = false; export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = false; export const allowConcurrentByDefault = false;
export const enableCustomElementPropertySupport = false; export const enableCustomElementPropertySupport = false;

View File

@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
export const disableSchedulerTimeoutInWorkLoop = false; export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false; export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false; export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = __EXPERIMENTAL__; export const enableUnifiedSyncLane = __EXPERIMENTAL__;
export const allowConcurrentByDefault = false; export const allowConcurrentByDefault = false;
export const enableCustomElementPropertySupport = false; export const enableCustomElementPropertySupport = false;

View File

@ -53,6 +53,7 @@ export const enableUseRefAccessWarning = false;
export const disableSchedulerTimeoutInWorkLoop = false; export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false; export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false; export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = false; export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true; export const allowConcurrentByDefault = true;

View File

@ -54,6 +54,7 @@ export const enableUseRefAccessWarning = false;
export const disableSchedulerTimeoutInWorkLoop = false; export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false; export const enableLazyContextPropagation = false;
export const enableLegacyHidden = false; export const enableLegacyHidden = false;
export const enableSyncDefaultUpdates = true;
export const enableUnifiedSyncLane = false; export const enableUnifiedSyncLane = false;
export const allowConcurrentByDefault = true; export const allowConcurrentByDefault = true;
export const enableCustomElementPropertySupport = false; export const enableCustomElementPropertySupport = false;

View File

@ -20,6 +20,7 @@ export const enableUseRefAccessWarning = __VARIANT__;
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const enableLazyContextPropagation = __VARIANT__; export const enableLazyContextPropagation = __VARIANT__;
export const enableSyncDefaultUpdates = __VARIANT__;
export const enableUnifiedSyncLane = __VARIANT__; export const enableUnifiedSyncLane = __VARIANT__;
export const enableTransitionTracing = __VARIANT__; export const enableTransitionTracing = __VARIANT__;
export const enableCustomElementPropertySupport = __VARIANT__; export const enableCustomElementPropertySupport = __VARIANT__;

View File

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

View File

@ -339,9 +339,13 @@ describe('useSubscription', () => {
// Start React update, but don't finish // Start React update, but don't finish
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />); renderer.update(<Parent observed={observableB} />);
}); }
await waitFor(['Child: b-0']); await waitFor(['Child: b-0']);
expect(log).toEqual(['Parent.componentDidMount']); expect(log).toEqual(['Parent.componentDidMount']);
@ -443,9 +447,13 @@ describe('useSubscription', () => {
// Start React update, but don't finish // Start React update, but don't finish
await act(async () => { await act(async () => {
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
} else {
renderer.update(<Parent observed={observableB} />); renderer.update(<Parent observed={observableB} />);
}); }
await waitFor(['Child: b-0']); await waitFor(['Child: b-0']);
expect(log).toEqual([]); expect(log).toEqual([]);
@ -624,13 +632,21 @@ describe('useSubscription', () => {
// Interrupt with a second mutation "C" -> "D". // Interrupt with a second mutation "C" -> "D".
// This update will not be eagerly evaluated, // This update will not be eagerly evaluated,
// but useSubscription() should eagerly close over the updated value to avoid tearing. // 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'); mutate('C');
}); }
await waitFor(['render:first:C', 'render:second:C']); await waitFor(['render:first:C', 'render:second:C']);
React.startTransition(() => { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
mutate('D');
});
} else {
mutate('D'); mutate('D');
}); }
await waitForAll(['render:first:D', 'render:second:D']); await waitForAll(['render:first:D', 'render:second:D']);
// No more pending updates // No more pending updates