@samitouri / QOS-React-2 / commits / f193213d29

Add a regression test for an infinite suspense + Fix (#27703)

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>

Tianyu Yao committed Dec 8, 2023 at 09:21 UTC f193213d29e7fabf68b21951c4b96701aeaecbf8
2 files changed +75 -1
packages/react-reconciler/src/ReactFiberLane.js
+3 -1
@@ -423,7 +423,9 @@ export function markStarvedLanesAsExpired(
423 // We exclude retry lanes because those must always be time sliced, in order
424 // to unwrap uncached promises.
425 // TODO: Write a test for this
426 - let lanes = pendingLanes & ~RetryLanes;
426 + let lanes = enableRetryLaneExpiration
427 + ? pendingLanes
428 + : pendingLanes & ~RetryLanes;
429 while (lanes > 0) {
430 const index = pickArbitraryLaneIndex(lanes);
431 const lane = 1 << index;
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+72
@@ -5,6 +5,7 @@ let Scheduler;
5 let act;
6 let waitFor;
7 let waitForAll;
8 +let waitForMicrotasks;
9 let assertLog;
10 let waitForPaint;
11 let Suspense;
@@ -29,6 +30,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
30 waitFor = InternalTestUtils.waitFor;
31 waitForAll = InternalTestUtils.waitForAll;
32 waitForPaint = InternalTestUtils.waitForPaint;
33 + waitForMicrotasks = InternalTestUtils.waitForMicrotasks;
34 assertLog = InternalTestUtils.assertLog;
35
36 getCacheForType = React.unstable_getCacheForType;
@@ -4008,4 +4010,74 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4010 );
4011 },
4012 );
4013 +
4014 + // @gate enableLegacyCache && enableRetryLaneExpiration
4015 + it('recurring updates in siblings should not block expensive content in suspense boundary from committing', async () => {
4016 + const {useState} = React;
4017 +
4018 + let setText;
4019 + function UpdatingText() {
4020 + const [text, _setText] = useState('1');
4021 + setText = _setText;
4022 + return <Text text={text} />;
4023 + }
4024 +
4025 + function ExpensiveText({text, ms}) {
4026 + Scheduler.log(text);
4027 + Scheduler.unstable_advanceTime(ms);
4028 + return <span prop={text} />;
4029 + }
4030 +
4031 + function App() {
4032 + return (
4033 + <>
4034 + <UpdatingText />
4035 + <Suspense fallback={<Text text="Loading..." />}>
4036 + <AsyncText text="Async" />
4037 + <ExpensiveText text="A" ms={1000} />
4038 + <ExpensiveText text="B" ms={3999} />
4039 + <ExpensiveText text="C" ms={100000} />
4040 + </Suspense>
4041 + </>
4042 + );
4043 + }
4044 +
4045 + const root = ReactNoop.createRoot();
4046 + root.render(<App />);
4047 + await waitForAll(['1', 'Suspend! [Async]', 'Loading...']);
4048 + expect(root).toMatchRenderedOutput(
4049 + <>
4050 + <span prop="1" />
4051 + <span prop="Loading..." />
4052 + </>,
4053 + );
4054 +
4055 + await resolveText('Async');
4056 + expect(root).toMatchRenderedOutput(
4057 + <>
4058 + <span prop="1" />
4059 + <span prop="Loading..." />
4060 + </>,
4061 + );
4062 +
4063 + await waitFor(['Async', 'A', 'B']);
4064 + ReactNoop.expire(100000);
4065 + await advanceTimers(100000);
4066 + setText('2');
4067 + await waitForPaint(['2']);
4068 +
4069 + await waitForMicrotasks();
4070 + Scheduler.unstable_flushNumberOfYields(1);
4071 + assertLog(['Async', 'A', 'B', 'C']);
4072 +
4073 + expect(root).toMatchRenderedOutput(
4074 + <>
4075 + <span prop="2" />
4076 + <span prop="Async" />
4077 + <span prop="A" />
4078 + <span prop="B" />
4079 + <span prop="C" />
4080 + </>,
4081 + );
4082 + });
4083 });