mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Re-enable risky work loop changes (#16771)
The stack of PRs in #16743 was reverted. This adds them back.
This commit is contained in:
@@ -177,6 +177,7 @@ import {
|
||||
retryDehydratedSuspenseBoundary,
|
||||
scheduleWork,
|
||||
renderDidSuspendDelayIfPossible,
|
||||
markUnprocessedUpdateTime,
|
||||
} from './ReactFiberWorkLoop';
|
||||
|
||||
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
@@ -2709,6 +2710,11 @@ function bailoutOnAlreadyFinishedWork(
|
||||
stopProfilerTimerIfRunning(workInProgress);
|
||||
}
|
||||
|
||||
const updateExpirationTime = workInProgress.expirationTime;
|
||||
if (updateExpirationTime !== NoWork) {
|
||||
markUnprocessedUpdateTime(updateExpirationTime);
|
||||
}
|
||||
|
||||
// Check if the children have any pending work.
|
||||
const childExpirationTime = workInProgress.childExpirationTime;
|
||||
if (childExpirationTime < renderExpirationTime) {
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
export type ExpirationTime = number;
|
||||
|
||||
export const NoWork = 0;
|
||||
// TODO: Think of a better name for Never.
|
||||
export const Never = 1;
|
||||
// TODO: Use the Idle expiration time for idle state updates
|
||||
export const Idle = 2;
|
||||
export const Sync = MAX_SIGNED_31_BIT_INT;
|
||||
export const Batched = Sync - 1;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
warnIfNotCurrentlyActingUpdatesInDev,
|
||||
warnIfNotScopedWithMatchingAct,
|
||||
markRenderEventTimeAndConfig,
|
||||
markUnprocessedUpdateTime,
|
||||
} from './ReactFiberWorkLoop';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -531,6 +532,7 @@ export function resetHooks(): void {
|
||||
// This is used to reset the state of this module when a component throws.
|
||||
// It's also called inside mountIndeterminateComponent if we determine the
|
||||
// component is a module-style component.
|
||||
|
||||
renderExpirationTime = NoWork;
|
||||
currentlyRenderingFiber = null;
|
||||
|
||||
@@ -755,6 +757,7 @@ function updateReducer<S, I, A>(
|
||||
// Update the remaining priority in the queue.
|
||||
if (updateExpirationTime > remainingExpirationTime) {
|
||||
remainingExpirationTime = updateExpirationTime;
|
||||
markUnprocessedUpdateTime(remainingExpirationTime);
|
||||
}
|
||||
} else {
|
||||
// This update does have sufficient priority.
|
||||
|
||||
+125
-7
@@ -14,6 +14,7 @@ import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
|
||||
import type {Thenable} from './ReactFiberWorkLoop';
|
||||
import type {Interaction} from 'scheduler/src/Tracing';
|
||||
import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
|
||||
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
|
||||
|
||||
import {noTimeout} from './ReactFiberHostConfig';
|
||||
import {createHostRootFiber} from './ReactFiber';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
enableSuspenseCallback,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {unstable_getThreadID} from 'scheduler/tracing';
|
||||
import {NoPriority} from './SchedulerWithReactIntegration';
|
||||
|
||||
// TODO: This should be lifted into the renderer.
|
||||
export type Batch = {
|
||||
@@ -69,12 +71,20 @@ type BaseFiberRootProperties = {|
|
||||
callbackNode: *,
|
||||
// Expiration of the callback associated with this root
|
||||
callbackExpirationTime: ExpirationTime,
|
||||
// Priority of the callback associated with this root
|
||||
callbackPriority: ReactPriorityLevel,
|
||||
// The earliest pending expiration time that exists in the tree
|
||||
firstPendingTime: ExpirationTime,
|
||||
// The latest pending expiration time that exists in the tree
|
||||
lastPendingTime: ExpirationTime,
|
||||
// The time at which a suspended component pinged the root to render again
|
||||
pingTime: ExpirationTime,
|
||||
// The earliest suspended expiration time that exists in the tree
|
||||
firstSuspendedTime: ExpirationTime,
|
||||
// The latest suspended expiration time that exists in the tree
|
||||
lastSuspendedTime: ExpirationTime,
|
||||
// The next known expiration time after the suspended range
|
||||
nextKnownPendingLevel: ExpirationTime,
|
||||
// The latest time at which a suspended component pinged the root to
|
||||
// render again
|
||||
lastPingedTime: ExpirationTime,
|
||||
lastExpiredTime: ExpirationTime,
|
||||
|};
|
||||
|
||||
// The following attributes are only used by interaction tracing builds.
|
||||
@@ -117,10 +127,13 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.hydrate = hydrate;
|
||||
this.firstBatch = null;
|
||||
this.callbackNode = null;
|
||||
this.callbackExpirationTime = NoWork;
|
||||
this.callbackPriority = NoPriority;
|
||||
this.firstPendingTime = NoWork;
|
||||
this.lastPendingTime = NoWork;
|
||||
this.pingTime = NoWork;
|
||||
this.firstSuspendedTime = NoWork;
|
||||
this.lastSuspendedTime = NoWork;
|
||||
this.nextKnownPendingLevel = NoWork;
|
||||
this.lastPingedTime = NoWork;
|
||||
this.lastExpiredTime = NoWork;
|
||||
|
||||
if (enableSchedulerTracing) {
|
||||
this.interactionThreadID = unstable_getThreadID();
|
||||
@@ -151,3 +164,108 @@ export function createFiberRoot(
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export function isRootSuspendedAtTime(
|
||||
root: FiberRoot,
|
||||
expirationTime: ExpirationTime,
|
||||
): boolean {
|
||||
const firstSuspendedTime = root.firstSuspendedTime;
|
||||
const lastSuspendedTime = root.lastSuspendedTime;
|
||||
return (
|
||||
firstSuspendedTime !== NoWork &&
|
||||
(firstSuspendedTime >= expirationTime &&
|
||||
lastSuspendedTime <= expirationTime)
|
||||
);
|
||||
}
|
||||
|
||||
export function markRootSuspendedAtTime(
|
||||
root: FiberRoot,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const firstSuspendedTime = root.firstSuspendedTime;
|
||||
const lastSuspendedTime = root.lastSuspendedTime;
|
||||
if (firstSuspendedTime < expirationTime) {
|
||||
root.firstSuspendedTime = expirationTime;
|
||||
}
|
||||
if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) {
|
||||
root.lastSuspendedTime = expirationTime;
|
||||
}
|
||||
|
||||
if (expirationTime <= root.lastPingedTime) {
|
||||
root.lastPingedTime = NoWork;
|
||||
}
|
||||
|
||||
if (expirationTime <= root.lastExpiredTime) {
|
||||
root.lastExpiredTime = NoWork;
|
||||
}
|
||||
}
|
||||
|
||||
export function markRootUpdatedAtTime(
|
||||
root: FiberRoot,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
// Update the range of pending times
|
||||
const firstPendingTime = root.firstPendingTime;
|
||||
if (expirationTime > firstPendingTime) {
|
||||
root.firstPendingTime = expirationTime;
|
||||
}
|
||||
|
||||
// Update the range of suspended times. Treat everything lower priority or
|
||||
// equal to this update as unsuspended.
|
||||
const firstSuspendedTime = root.firstSuspendedTime;
|
||||
if (firstSuspendedTime !== NoWork) {
|
||||
if (expirationTime >= firstSuspendedTime) {
|
||||
// The entire suspended range is now unsuspended.
|
||||
root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
|
||||
} else if (expirationTime >= root.lastSuspendedTime) {
|
||||
root.lastSuspendedTime = expirationTime + 1;
|
||||
}
|
||||
|
||||
// This is a pending level. Check if it's higher priority than the next
|
||||
// known pending level.
|
||||
if (expirationTime > root.nextKnownPendingLevel) {
|
||||
root.nextKnownPendingLevel = expirationTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function markRootFinishedAtTime(
|
||||
root: FiberRoot,
|
||||
finishedExpirationTime: ExpirationTime,
|
||||
remainingExpirationTime: ExpirationTime,
|
||||
): void {
|
||||
// Update the range of pending times
|
||||
root.firstPendingTime = remainingExpirationTime;
|
||||
|
||||
// Update the range of suspended times. Treat everything higher priority or
|
||||
// equal to this update as unsuspended.
|
||||
if (finishedExpirationTime <= root.lastSuspendedTime) {
|
||||
// The entire suspended range is now unsuspended.
|
||||
root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
|
||||
} else if (finishedExpirationTime <= root.firstSuspendedTime) {
|
||||
// Part of the suspended range is now unsuspended. Narrow the range to
|
||||
// include everything between the unsuspended time (non-inclusive) and the
|
||||
// last suspended time.
|
||||
root.firstSuspendedTime = finishedExpirationTime - 1;
|
||||
}
|
||||
|
||||
if (finishedExpirationTime <= root.lastPingedTime) {
|
||||
// Clear the pinged time
|
||||
root.lastPingedTime = NoWork;
|
||||
}
|
||||
|
||||
if (finishedExpirationTime <= root.lastExpiredTime) {
|
||||
// Clear the expired time
|
||||
root.lastExpiredTime = NoWork;
|
||||
}
|
||||
}
|
||||
|
||||
export function markRootExpiredAtTime(
|
||||
root: FiberRoot,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const lastExpiredTime = root.lastExpiredTime;
|
||||
if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) {
|
||||
root.lastExpiredTime = expirationTime;
|
||||
}
|
||||
}
|
||||
|
||||
+586
-416
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -103,7 +103,10 @@ import {
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {StrictMode} from './ReactTypeOfMode';
|
||||
import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
markRenderEventTimeAndConfig,
|
||||
markUnprocessedUpdateTime,
|
||||
} from './ReactFiberWorkLoop';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
@@ -580,6 +583,7 @@ export function processUpdateQueue<State>(
|
||||
// dealt with the props. Context in components that specify
|
||||
// shouldComponentUpdate is tricky; but we'll have to account for
|
||||
// that regardless.
|
||||
markUnprocessedUpdateTime(newExpirationTime);
|
||||
workInProgress.expirationTime = newExpirationTime;
|
||||
workInProgress.memoizedState = resultState;
|
||||
|
||||
|
||||
+78
-47
@@ -243,11 +243,20 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
// This update is in a separate batch
|
||||
ReactNoop.render(<App isBroken={false} />, onCommit);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The first render fails. But because there's a lower priority pending
|
||||
// update, it doesn't throw.
|
||||
'error',
|
||||
// Now we retry at the lower priority. This time it succeeds.
|
||||
]);
|
||||
|
||||
// React will try to recover by rendering all the pending updates in a
|
||||
// single batch, synchronously. This time it succeeds.
|
||||
//
|
||||
// This tells Scheduler to render a single unit of work. Because the render
|
||||
// to recover from the error is synchronous, this should be enough to
|
||||
// finish the rest of the work.
|
||||
Scheduler.unstable_flushNumberOfYields(1);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'success',
|
||||
// Nothing commits until the second update completes.
|
||||
'commit',
|
||||
@@ -256,54 +265,80 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
|
||||
});
|
||||
|
||||
it('on error, retries at a lower priority using the expiration of higher priority', () => {
|
||||
class Parent extends React.Component {
|
||||
state = {hideChild: false};
|
||||
componentDidUpdate() {
|
||||
Scheduler.unstable_yieldValue('commit: ' + this.state.hideChild);
|
||||
}
|
||||
render() {
|
||||
if (this.state.hideChild) {
|
||||
Scheduler.unstable_yieldValue('(empty)');
|
||||
return <span prop="(empty)" />;
|
||||
}
|
||||
return <Child isBroken={this.props.childIsBroken} />;
|
||||
}
|
||||
}
|
||||
|
||||
function Child(props) {
|
||||
it('does not include offscreen work when retrying after an error', () => {
|
||||
function App(props) {
|
||||
if (props.isBroken) {
|
||||
Scheduler.unstable_yieldValue('Error!');
|
||||
throw new Error('Error!');
|
||||
Scheduler.unstable_yieldValue('error');
|
||||
throw new Error('Oops!');
|
||||
}
|
||||
Scheduler.unstable_yieldValue('Child');
|
||||
return <span prop="Child" />;
|
||||
Scheduler.unstable_yieldValue('success');
|
||||
return (
|
||||
<>
|
||||
Everything is fine
|
||||
<div hidden={true}>
|
||||
<div>Offscreen content</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
const parent = React.createRef(null);
|
||||
ReactNoop.render(<Parent ref={parent} childIsBroken={false} />);
|
||||
expect(Scheduler).toFlushAndYield(['Child']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
|
||||
function onCommit() {
|
||||
Scheduler.unstable_yieldValue('commit');
|
||||
}
|
||||
|
||||
// Schedule a low priority update to hide the child
|
||||
parent.current.setState({hideChild: true});
|
||||
function interrupt() {
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.renderToRootWithID(null, 'other-root');
|
||||
});
|
||||
}
|
||||
|
||||
// Before the low priority update is flushed, synchronously trigger an
|
||||
// error in the child.
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.render(<Parent ref={parent} childIsBroken={true} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// First the sync update triggers an error
|
||||
'Error!',
|
||||
// Because there's a pending low priority update, we restart at the
|
||||
// lower priority. This hides the children, suppressing the error.
|
||||
'(empty)',
|
||||
// Now the tree can commit.
|
||||
'commit: true',
|
||||
ReactNoop.render(<App isBroken={true} />, onCommit);
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['error']);
|
||||
interrupt();
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
|
||||
// This update is in a separate batch
|
||||
ReactNoop.render(<App isBroken={false} />, onCommit);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The first render fails. But because there's a lower priority pending
|
||||
// update, it doesn't throw.
|
||||
'error',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
|
||||
|
||||
// React will try to recover by rendering all the pending updates in a
|
||||
// single batch, synchronously. This time it succeeds.
|
||||
//
|
||||
// This tells Scheduler to render a single unit of work. Because the render
|
||||
// to recover from the error is synchronous, this should be enough to
|
||||
// finish the rest of the work.
|
||||
Scheduler.unstable_flushNumberOfYields(1);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'success',
|
||||
// Nothing commits until the second update completes.
|
||||
'commit',
|
||||
'commit',
|
||||
]);
|
||||
// This should not include the offscreen content
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
Everything is fine
|
||||
<div hidden={true} />
|
||||
</>,
|
||||
);
|
||||
|
||||
// The offscreen content finishes in a subsequent render
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
Everything is fine
|
||||
<div hidden={true}>
|
||||
<div>Offscreen content</div>
|
||||
</div>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
it('retries one more time before handling error', () => {
|
||||
@@ -346,10 +381,6 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
// TODO: This is currently unobservable, but will be once we lift renderRoot
|
||||
// and commitRoot into the renderer.
|
||||
// it("does not retry synchronously if there's an update between complete and commit");
|
||||
|
||||
it('calls componentDidCatch multiple times for multiple errors', () => {
|
||||
let id = 0;
|
||||
class BadMount extends React.Component {
|
||||
|
||||
@@ -263,6 +263,274 @@ describe('ReactSuspense', () => {
|
||||
expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling');
|
||||
});
|
||||
|
||||
it(
|
||||
'interrupts current render if something already suspended with a ' +
|
||||
"delay, and then subsequently there's a lower priority update",
|
||||
() => {
|
||||
const root = ReactTestRenderer.create(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />} />
|
||||
<Text text="Initial" />
|
||||
</>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['Initial']);
|
||||
expect(root).toMatchRenderedOutput('Initial');
|
||||
|
||||
// The update will suspend.
|
||||
root.update(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" ms={2000} />
|
||||
</Suspense>
|
||||
<Text text="After Suspense" />
|
||||
<Text text="Sibling" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Yield past the Suspense boundary but don't complete the last sibling.
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Suspend! [Async]',
|
||||
'Loading...',
|
||||
'After Suspense',
|
||||
]);
|
||||
|
||||
// Receives a lower priority update before the current render phase
|
||||
// has completed.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
root.update(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />} />
|
||||
<Text text="Updated" />
|
||||
</>,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Initial');
|
||||
|
||||
// Render the update, instead of continuing
|
||||
expect(Scheduler).toFlushAndYield(['Updated']);
|
||||
expect(root).toMatchRenderedOutput('Updated');
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'interrupts current render when something suspends with a ' +
|
||||
"delay and we've already skipped over a lower priority update in " +
|
||||
'a parent',
|
||||
() => {
|
||||
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} />);
|
||||
expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
|
||||
expect(root).toMatchRenderedOutput('A0B0C0');
|
||||
|
||||
// This update will suspend.
|
||||
root.update(<App shouldSuspend={true} step={1} />);
|
||||
|
||||
// Need to move into the next async bucket.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
// Do a bit of work, then interrupt to trigger a restart.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A1']);
|
||||
interrupt();
|
||||
|
||||
// Schedule another update. This will have lower priority because of
|
||||
// the interrupt trick above.
|
||||
root.update(<App shouldSuspend={false} step={2} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// 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.
|
||||
expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);
|
||||
|
||||
expect(root).toMatchRenderedOutput('A2B2C2');
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'interrupts current render when something suspends with a ' +
|
||||
"delay and we've already bailed out lower priority update in " +
|
||||
'a parent',
|
||||
async () => {
|
||||
// This is similar to the previous test case, except this covers when
|
||||
// React completely bails out on the parent component, without processing
|
||||
// the update queue.
|
||||
|
||||
const {useState} = React;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
let setShouldSuspend;
|
||||
function Async() {
|
||||
const [shouldSuspend, _setShouldSuspend] = useState(false);
|
||||
setShouldSuspend = _setShouldSuspend;
|
||||
return (
|
||||
<>
|
||||
<Text text="A" />
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
{shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
|
||||
</Suspense>
|
||||
<Text text="B" />
|
||||
<Text text="C" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let setShouldHideInParent;
|
||||
function App() {
|
||||
const [shouldHideInParent, _setShouldHideInParent] = useState(false);
|
||||
setShouldHideInParent = _setShouldHideInParent;
|
||||
Scheduler.unstable_yieldValue(
|
||||
'shouldHideInParent: ' + shouldHideInParent,
|
||||
);
|
||||
return shouldHideInParent ? <Text text="(empty)" /> : <Async />;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(null, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
await ReactTestRenderer.act(async () => {
|
||||
root.update(<App />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'shouldHideInParent: false',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('ABC');
|
||||
|
||||
// This update will suspend.
|
||||
setShouldSuspend(true);
|
||||
|
||||
// Need to move into the next async bucket.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
// Do a bit of work, then interrupt to trigger a restart.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
interrupt();
|
||||
// Should not have committed loading state
|
||||
expect(root).toMatchRenderedOutput('ABC');
|
||||
|
||||
// Schedule another update. This will have lower priority because of
|
||||
// the interrupt trick above.
|
||||
setShouldHideInParent(true);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// Should have restarted the first update, because of the interruption
|
||||
'A',
|
||||
'Suspend! [Async]',
|
||||
'Loading...',
|
||||
'B',
|
||||
]);
|
||||
|
||||
// Should not have committed loading state
|
||||
expect(root).toMatchRenderedOutput('ABC');
|
||||
|
||||
// After suspending, should abort the first update and switch to the
|
||||
// second update.
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'shouldHideInParent: true',
|
||||
'(empty)',
|
||||
]);
|
||||
|
||||
expect(root).toMatchRenderedOutput('(empty)');
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'interrupts current render when something suspends with a ' +
|
||||
'delay, and a parent received an update after it completed',
|
||||
() => {
|
||||
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} />);
|
||||
expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
|
||||
expect(root).toMatchRenderedOutput('A0B0C0');
|
||||
|
||||
// This update will suspend.
|
||||
root.update(<App shouldSuspend={true} step={1} />);
|
||||
// Flush past the root, but stop before the async component.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A1']);
|
||||
|
||||
// Schedule an update on the root, which already completed.
|
||||
root.update(<App shouldSuspend={false} step={2} />);
|
||||
// We'll keep working on the existing update.
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// Now the async component suspends
|
||||
'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.
|
||||
expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);
|
||||
|
||||
expect(root).toMatchRenderedOutput('A2B2C2');
|
||||
},
|
||||
);
|
||||
|
||||
it('mounts a lazy class component in non-concurrent mode', async () => {
|
||||
class Class extends React.Component {
|
||||
componentDidMount() {
|
||||
|
||||
+66
-1
@@ -494,7 +494,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
return <Text text="(empty)" />;
|
||||
}
|
||||
return (
|
||||
<Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<AsyncText ms={2000} text="Async" />
|
||||
</Suspense>
|
||||
);
|
||||
@@ -518,6 +518,71 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
|
||||
});
|
||||
|
||||
it('tries each subsequent level after suspending', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
function App({step, shouldSuspend}) {
|
||||
return (
|
||||
<Suspense fallback="Loading...">
|
||||
<Text text="Sibling" />
|
||||
{shouldSuspend ? (
|
||||
<AsyncText ms={10000} text={'Step ' + step} />
|
||||
) : (
|
||||
<Text text={'Step ' + step} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function interrupt() {
|
||||
// React has a heuristic to batch all updates that occur within the same
|
||||
// event. This is a trick to circumvent that heuristic.
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.renderToRootWithID(null, 'other-root');
|
||||
});
|
||||
}
|
||||
|
||||
// Mount the Suspense boundary without suspending, so that the subsequent
|
||||
// updates suspend with a delay.
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<App step={0} shouldSuspend={false} />);
|
||||
});
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
|
||||
|
||||
// Schedule an update at several distinct expiration times
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<App step={1} shouldSuspend={true} />);
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
|
||||
root.render(<App step={2} shouldSuspend={true} />);
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
|
||||
root.render(<App step={3} shouldSuspend={true} />);
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
|
||||
root.render(<App step={4} shouldSuspend={false} />);
|
||||
});
|
||||
|
||||
// Should suspend at each distinct level
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Sibling',
|
||||
'Suspend! [Step 1]',
|
||||
'Sibling',
|
||||
'Suspend! [Step 2]',
|
||||
'Sibling',
|
||||
'Suspend! [Step 3]',
|
||||
'Sibling',
|
||||
'Step 4',
|
||||
]);
|
||||
});
|
||||
|
||||
it('forces an expiration after an update times out', async () => {
|
||||
ReactNoop.render(
|
||||
<Fragment>
|
||||
|
||||
+4
-1
@@ -422,7 +422,10 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
|
||||
`;
|
||||
|
||||
exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
|
||||
"⚛ (Committing Changes)
|
||||
"⚛ (React Tree Reconciliation: Completed Root)
|
||||
⚛ Foo [mount]
|
||||
|
||||
⚛ (Committing Changes)
|
||||
⚛ (Committing Snapshot Effects: 0 Total)
|
||||
⚛ (Committing Host Effects: 1 Total)
|
||||
⚛ (Calling Lifecycle Methods: 0 Total)
|
||||
|
||||
Reference in New Issue
Block a user