Skip to content

Commit c404414

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

File tree

3 files changed

+47
-2
lines changed

3 files changed

+47
-2
lines changed

compat/src/suspense.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
180180
};
181181

182182
Suspense.prototype.componentWillUnmount = function () {
183-
this._suspenders = [];
183+
this._suspenders = this.state._suspended = this._detachOnNextRender = null;
184184
};
185185

186186
/**

compat/test/browser/suspense.test.js

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

418+
it('should 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+
const rerenderDetachedChild = () => {
456+
childInstance.setState({ value: 1, suspend: false });
457+
rerender();
458+
};
459+
460+
expect(rerenderDetachedChild).to.not.throw();
461+
});
462+
418463
it('should properly call lifecycle methods of an initially suspending component', () => {
419464
/** @type {() => Promise<void>} */
420465
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)