Skip to content

Commit 795868a

Browse files
committed
Fix rare scenario where an unmounted suspended tree would resolve
1 parent 3ab5c6f commit 795868a

File tree

3 files changed

+47
-2
lines changed

3 files changed

+47
-2
lines changed

compat/src/suspense.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
147147
if (!--c._pendingSuspensionCount) {
148148
// If the suspension was during hydration we don't need to restore the
149149
// suspended children into the _children array
150-
if (c.state._suspended) {
150+
if (c.state._suspended && c.state._suspended._component) {
151151
const suspendedVNode = c.state._suspended;
152152
c._vnode._children[0] = removeOriginal(
153153
suspendedVNode,
@@ -181,6 +181,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
181181

182182
Suspense.prototype.componentWillUnmount = function () {
183183
this._suspenders = [];
184+
this.state._suspended = this._detachOnNextRender = null;
184185
};
185186

186187
/**
@@ -236,6 +237,7 @@ Suspense.prototype.render = function (props, state) {
236237
* @returns {((unsuspend: () => void) => void)?}
237238
*/
238239
export function suspended(vnode) {
240+
if (!vnode._parent) return null;
239241
/** @type {import('./internal').Component} */
240242
let component = vnode._parent._component;
241243
return component && component._suspended && component._suspended(vnode);

compat/test/browser/suspense.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,49 @@ describe('suspense', () => {
415415
});
416416
});
417417

418+
it('should not crash when suspended child updates after unmount', () => {
419+
/** @type {Component | null} */
420+
let childInstance = null;
421+
const neverResolvingPromise = new Promise(() => {});
422+
423+
class ThrowingChild extends Component {
424+
constructor(props) {
425+
super(props);
426+
this.state = { suspend: false, value: 0 };
427+
childInstance = this;
428+
}
429+
430+
render(props, state) {
431+
if (state.suspend) {
432+
throw neverResolvingPromise;
433+
}
434+
435+
return <div>value:{state.value}</div>;
436+
}
437+
}
438+
439+
render(
440+
<Suspense fallback={<div>Suspended...</div>}>
441+
<ThrowingChild />
442+
</Suspense>,
443+
scratch
444+
);
445+
446+
expect(childInstance).to.not.equal(null);
447+
expect(scratch.innerHTML).to.equal('<div>value:0</div>');
448+
449+
act(() => childInstance.setState({ suspend: true }));
450+
rerender();
451+
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');
452+
453+
render(null, scratch);
454+
455+
childInstance.setState({ value: 1 });
456+
rerender();
457+
458+
expect(scratch.innerHTML).to.equal('');
459+
});
460+
418461
it('should properly call lifecycle methods of an initially suspending component', () => {
419462
/** @type {() => Promise<void>} */
420463
let resolve;

src/component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function renderComponent(component) {
126126
commitQueue = [],
127127
refQueue = [];
128128

129-
if (component._parentDom) {
129+
if (component._parentDom && oldVNode._parent) {
130130
const newVNode = assign({}, oldVNode);
131131
newVNode._original = oldVNode._original + 1;
132132
if (options.vnode) options.vnode(newVNode);

0 commit comments

Comments
 (0)