Skip to content

Commit

Permalink
Add a regression test for an infinite suspense + Fix (facebook#27703)
Browse files Browse the repository at this point in the history
Add a regression test for the [minimal
repro](https://codesandbox.io/s/react-18-suspense-state-never-resolving-bug-hmlny5?file=/src/App.js)
from @kassens

And includes the fix from @acdlite: 
> This is another place we special-case Retry lanes to opt them out of
expiration. The reason is we rely on time slicing to unwrap uncached
promises (i.e. async functions during render). Since that ability is
still experimental, and enableRetryLaneExpiration is Meta-only, we can
remove the special case when enableRetryLaneExpiration is on, for now.

---------

Co-authored-by: Andrew Clark <git@andrewclark.io>
  • Loading branch information
2 people authored and AndyPengc12 committed Apr 15, 2024
1 parent 850d4f7 commit 6b26b2d
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,9 @@ export function markStarvedLanesAsExpired(
// We exclude retry lanes because those must always be time sliced, in order
// to unwrap uncached promises.
// TODO: Write a test for this
let lanes = pendingLanes & ~RetryLanes;
let lanes = enableRetryLaneExpiration
? pendingLanes
: pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ let Scheduler;
let act;
let waitFor;
let waitForAll;
let waitForMicrotasks;
let assertLog;
let waitForPaint;
let Suspense;
Expand All @@ -29,6 +30,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
waitForMicrotasks = InternalTestUtils.waitForMicrotasks;
assertLog = InternalTestUtils.assertLog;

getCacheForType = React.unstable_getCacheForType;
Expand Down Expand Up @@ -4008,4 +4010,74 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
},
);

// @gate enableLegacyCache && enableRetryLaneExpiration
it('recurring updates in siblings should not block expensive content in suspense boundary from committing', async () => {
const {useState} = React;

let setText;
function UpdatingText() {
const [text, _setText] = useState('1');
setText = _setText;
return <Text text={text} />;
}

function ExpensiveText({text, ms}) {
Scheduler.log(text);
Scheduler.unstable_advanceTime(ms);
return <span prop={text} />;
}

function App() {
return (
<>
<UpdatingText />
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" />
<ExpensiveText text="A" ms={1000} />
<ExpensiveText text="B" ms={3999} />
<ExpensiveText text="C" ms={100000} />
</Suspense>
</>
);
}

const root = ReactNoop.createRoot();
root.render(<App />);
await waitForAll(['1', 'Suspend! [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput(
<>
<span prop="1" />
<span prop="Loading..." />
</>,
);

await resolveText('Async');
expect(root).toMatchRenderedOutput(
<>
<span prop="1" />
<span prop="Loading..." />
</>,
);

await waitFor(['Async', 'A', 'B']);
ReactNoop.expire(100000);
await advanceTimers(100000);
setText('2');
await waitForPaint(['2']);

await waitForMicrotasks();
Scheduler.unstable_flushNumberOfYields(1);
assertLog(['Async', 'A', 'B', 'C']);

expect(root).toMatchRenderedOutput(
<>
<span prop="2" />
<span prop="Async" />
<span prop="A" />
<span prop="B" />
<span prop="C" />
</>,
);
});
});

0 comments on commit 6b26b2d

Please sign in to comment.