Skip to content

Commit 2def7b3

Browse files
authored
More robust fix for #18515 (#18535)
* Add another test for #18515 using pings Adds a regression test for the same underlying bug as #18515 but using pings. Test already passes, but I confirmed it fails if you revert the fix in #18515. * Set nextPendingLevel after commit, too
1 parent 948fad3 commit 2def7b3

File tree

2 files changed

+121
-11
lines changed

2 files changed

+121
-11
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

+8-11
Original file line numberDiff line numberDiff line change
@@ -679,9 +679,10 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
679679

680680
// We now have a consistent tree. The next step is either to commit it,
681681
// or, if something suspended, wait to commit it after a timeout.
682-
const finishedWork: Fiber = ((root.finishedWork =
683-
root.current.alternate): any);
682+
const finishedWork: Fiber = (root.current.alternate: any);
683+
root.finishedWork = finishedWork;
684684
root.finishedExpirationTime = expirationTime;
685+
root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
685686
finishConcurrentRender(root, finishedWork, exitStatus, expirationTime);
686687
}
687688

@@ -717,9 +718,6 @@ function finishConcurrentRender(
717718
case RootSuspended: {
718719
markRootSuspendedAtTime(root, expirationTime);
719720
const lastSuspendedTime = root.lastSuspendedTime;
720-
if (expirationTime === lastSuspendedTime) {
721-
root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
722-
}
723721

724722
// We have an acceptable loading state. We need to figure out if we
725723
// should immediately commit it or wait a bit.
@@ -792,9 +790,6 @@ function finishConcurrentRender(
792790
case RootSuspendedWithDelay: {
793791
markRootSuspendedAtTime(root, expirationTime);
794792
const lastSuspendedTime = root.lastSuspendedTime;
795-
if (expirationTime === lastSuspendedTime) {
796-
root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
797-
}
798793

799794
if (
800795
// do not delay if we're inside an act() scope
@@ -979,9 +974,10 @@ function performSyncWorkOnRoot(root) {
979974

980975
// We now have a consistent tree. Because this is a sync render, we
981976
// will commit it even if something suspended.
982-
root.finishedWork = (root.current.alternate: any);
977+
const finishedWork: Fiber = (root.current.alternate: any);
978+
root.finishedWork = finishedWork;
983979
root.finishedExpirationTime = expirationTime;
984-
980+
root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
985981
commitRoot(root);
986982

987983
// Before exiting, make sure there's a callback scheduled for the next
@@ -1176,6 +1172,8 @@ function prepareFreshStack(root, expirationTime) {
11761172
// to it later, in case the render we're about to start gets aborted.
11771173
// Generally we only reach this path via a ping, but we shouldn't assume
11781174
// that will always be the case.
1175+
// Note: This is defensive coding to prevent a pending commit from
1176+
// being dropped without being rescheduled. It shouldn't be necessary.
11791177
if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) {
11801178
root.lastPingedTime = lastSuspendedTime;
11811179
}
@@ -1772,7 +1770,6 @@ function commitRootImpl(root, renderPriorityLevel) {
17721770
root.callbackNode = null;
17731771
root.callbackExpirationTime = NoWork;
17741772
root.callbackPriority = NoPriority;
1775-
root.nextKnownPendingLevel = NoWork;
17761773

17771774
// Update the first and last pending times on this root. The new first
17781775
// pending time is whatever is left on the root fiber.

packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js

+113
Original file line numberDiff line numberDiff line change
@@ -3645,4 +3645,117 @@ describe('ReactSuspenseWithNoopRenderer', () => {
36453645
</>,
36463646
);
36473647
});
3648+
3649+
it('regression: ping at high priority causes update to be dropped', async () => {
3650+
const {useState, useTransition} = React;
3651+
3652+
let setTextA;
3653+
function A() {
3654+
const [textA, _setTextA] = useState('A');
3655+
setTextA = _setTextA;
3656+
return (
3657+
<Suspense fallback={<Text text="Loading..." />}>
3658+
<AsyncText text={textA} />
3659+
</Suspense>
3660+
);
3661+
}
3662+
3663+
let setTextB;
3664+
let startTransition;
3665+
function B() {
3666+
const [textB, _setTextB] = useState('B');
3667+
const [_startTransition] = useTransition({timeoutMs: 10000});
3668+
startTransition = _startTransition;
3669+
setTextB = _setTextB;
3670+
return (
3671+
<Suspense fallback={<Text text="Loading..." />}>
3672+
<AsyncText text={textB} />
3673+
</Suspense>
3674+
);
3675+
}
3676+
3677+
function App() {
3678+
return (
3679+
<>
3680+
<A />
3681+
<B />
3682+
</>
3683+
);
3684+
}
3685+
3686+
const root = ReactNoop.createRoot();
3687+
await ReactNoop.act(async () => {
3688+
await resolveText('A');
3689+
await resolveText('B');
3690+
root.render(<App />);
3691+
});
3692+
expect(Scheduler).toHaveYielded(['A', 'B']);
3693+
expect(root).toMatchRenderedOutput(
3694+
<>
3695+
<span prop="A" />
3696+
<span prop="B" />
3697+
</>,
3698+
);
3699+
3700+
await ReactNoop.act(async () => {
3701+
// Triggers suspense at normal pri
3702+
setTextA('A1');
3703+
// Triggers in an unrelated tree at a different pri
3704+
startTransition(() => {
3705+
// Update A again so that it doesn't suspend on A1. That way we can ping
3706+
// the A1 update without also pinging this one. This is a workaround
3707+
// because there's currently no way to render at a lower priority (B2)
3708+
// without including all updates at higher priority (A1).
3709+
setTextA('A2');
3710+
setTextB('B2');
3711+
});
3712+
});
3713+
expect(Scheduler).toHaveYielded([
3714+
'B',
3715+
'Suspend! [A1]',
3716+
'Loading...',
3717+
3718+
'Suspend! [A2]',
3719+
'Loading...',
3720+
'Suspend! [B2]',
3721+
'Loading...',
3722+
]);
3723+
expect(root).toMatchRenderedOutput(
3724+
<>
3725+
<span prop="A" />
3726+
<span prop="B" />
3727+
</>,
3728+
);
3729+
3730+
await ReactNoop.act(async () => {
3731+
resolveText('A1');
3732+
});
3733+
expect(Scheduler).toHaveYielded([
3734+
'Promise resolved [A1]',
3735+
'A1',
3736+
'Suspend! [A2]',
3737+
'Loading...',
3738+
'Suspend! [B2]',
3739+
'Loading...',
3740+
]);
3741+
expect(root).toMatchRenderedOutput(
3742+
<>
3743+
<span prop="A1" />
3744+
<span prop="B" />
3745+
</>,
3746+
);
3747+
3748+
// Commit the placeholder
3749+
Scheduler.unstable_advanceTime(20000);
3750+
await advanceTimers(20000);
3751+
3752+
expect(root).toMatchRenderedOutput(
3753+
<>
3754+
<span hidden={true} prop="A1" />
3755+
<span prop="Loading..." />
3756+
<span hidden={true} prop="B" />
3757+
<span prop="Loading..." />
3758+
</>,
3759+
);
3760+
});
36483761
});

0 commit comments

Comments
 (0)