Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Prevent crash when resuming class component inside SuspenseList #18432

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ import {
REACT_BLOCK_TYPE,
} from 'shared/ReactSymbols';

import {initializeUpdateQueue} from './ReactUpdateQueue';

let hasBadMapPolyfill;

if (__DEV__) {
Expand Down Expand Up @@ -546,7 +548,15 @@ export function resetWorkInProgress(
workInProgress.child = null;
workInProgress.memoizedProps = null;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is going to break other things that are likely not covered by tests. We’ve had many bugs related to not resetting enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to dive deeper to be able to fix this then. I tried initializeUpdateQueue(workInProgress) instead which caused a bunch of "Unexpected pop" or "Unexpected Fiber popped". Also tried

if (workInProgress.updateQueue !== null) {
  initializeUpdateQueue(workInProgress);
}

which caused the same issue

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind. VSCode has problems with flow and didn't realize initializeUpdateQueue was never defined because I imported from the same line as import type. The ReferenceError was just buried below 5 screens of console errors.

// updateQueue must never be null on ClassComponent or HostRoot
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line with

// This is always non-null on a ClassComponent or HostRoot

would it make more sense to move this logic into a resetUpdateQueue inside ReactUpdatedQueue.js?

if (
workInProgress.tag === ClassComponent ||
workInProgress.tag === HostRoot
) {
initializeUpdateQueue(workInProgress);
} else {
workInProgress.updateQueue = null;
}

workInProgress.dependencies = null;

Expand Down
9 changes: 9 additions & 0 deletions packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,15 @@ function resumeMountClassInstance(
nextContext = getMaskedContext(workInProgress, nextLegacyUnmaskedContext);
}

if (__DEV__) {
if (workInProgress.updateQueue === null) {
console.error(
'Class fiber is missing an updateQueue. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
typeof getDerivedStateFromProps === 'function' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2485,4 +2485,66 @@ describe('ReactSuspenseList', () => {
</>,
);
});

it('can resume class components when revealed together', async () => {
let A = createAsyncText('A');
let B = createAsyncText('B');

class ClassComponent extends React.Component {
render() {
return this.props.children;
}
}

function Foo() {
return (
<Suspense fallback={<Text text="Loading" />}>
<SuspenseList revealOrder="together">
<ClassComponent>
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
</ClassComponent>
<ClassComponent>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
</ClassComponent>
</SuspenseList>
</Suspense>
);
}

await A.resolve();

ReactNoop.render(<Foo />);

expect(Scheduler).toFlushAndYield([
'A',
'Suspend! [B]',
'Loading B',
'Loading A',
'Loading B',
]);

expect(ReactNoop).toMatchRenderedOutput(
<>
<span>Loading A</span>
<span>Loading B</span>
</>,
);

await B.resolve();

ReactNoop.render(<Foo />);

expect(Scheduler).toFlushAndYield(['A', 'B']);

expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
</>,
);
});
});