diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 6d5333a3b7..db99bcfb16 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -147,7 +147,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) { if (!--c._pendingSuspensionCount) { // If the suspension was during hydration we don't need to restore the // suspended children into the _children array - if (c.state._suspended) { + if (c.state._suspended && c.state._suspended._component) { const suspendedVNode = c.state._suspended; c._vnode._children[0] = removeOriginal( suspendedVNode, @@ -181,6 +181,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) { Suspense.prototype.componentWillUnmount = function () { this._suspenders = []; + this.state._suspended = this._detachOnNextRender = null; }; /** @@ -236,6 +237,7 @@ Suspense.prototype.render = function (props, state) { * @returns {((unsuspend: () => void) => void)?} */ export function suspended(vnode) { + if (!vnode._parent) return null; /** @type {import('./internal').Component} */ let component = vnode._parent._component; return component && component._suspended && component._suspended(vnode); diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js index 2998d80f30..39fede21b7 100644 --- a/compat/test/browser/suspense.test.js +++ b/compat/test/browser/suspense.test.js @@ -415,6 +415,49 @@ describe('suspense', () => { }); }); + it('should not crash when suspended child updates after unmount', () => { + /** @type {Component | null} */ + let childInstance = null; + const neverResolvingPromise = new Promise(() => {}); + + class ThrowingChild extends Component { + constructor(props) { + super(props); + this.state = { suspend: false, value: 0 }; + childInstance = this; + } + + render(props, state) { + if (state.suspend) { + throw neverResolvingPromise; + } + + return
value:{state.value}
; + } + } + + render( + Suspended...}> + + , + scratch + ); + + expect(childInstance).to.not.equal(null); + expect(scratch.innerHTML).to.equal('
value:0
'); + + act(() => childInstance.setState({ suspend: true })); + rerender(); + expect(scratch.innerHTML).to.equal('
Suspended...
'); + + render(null, scratch); + + childInstance.setState({ value: 1 }); + rerender(); + + expect(scratch.innerHTML).to.equal(''); + }); + it('should properly call lifecycle methods of an initially suspending component', () => { /** @type {() => Promise} */ let resolve; diff --git a/src/component.js b/src/component.js index 23f3f31896..b377c907c0 100644 --- a/src/component.js +++ b/src/component.js @@ -126,7 +126,7 @@ function renderComponent(component) { commitQueue = [], refQueue = []; - if (component._parentDom) { + if (component._parentDom && oldVNode._parent) { const newVNode = assign({}, oldVNode); newVNode._original = oldVNode._original + 1; if (options.vnode) options.vnode(newVNode);