diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js index 3526b96918..71fab2eb2c 100644 --- a/compat/test/browser/suspense-hydration.test.js +++ b/compat/test/browser/suspense-hydration.test.js @@ -97,6 +97,64 @@ describe('suspense hydration', () => { }); }); + it('Should not crash when oldVNode._children is null during shouldComponentUpdate optimization', () => { + const originalHtml = '
Hello
'; + scratch.innerHTML = originalHtml; + clearLog(); + + class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + return this.props.children; + } + } + + const [Lazy, resolve] = createLazy(); + function App() { + return ( + + + + + + ); + } + + hydrate(, scratch); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal(originalHtml); + expect(getLog()).to.deep.equal([]); + clearLog(); + + let i = 0; + class ThrowOrRender extends React.Component { + shouldComponentUpdate() { + return i === 0; + } + render() { + if (i === 0) { + i++; + throw new Error('Test error'); + } + return
Hello
; + } + } + + return resolve(ThrowOrRender).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal(originalHtml); + clearLog(); + }); + }); + it('should leave DOM untouched when suspending while hydrating', () => { scratch.innerHTML = '
Hello
'; clearLog(); diff --git a/src/diff/index.js b/src/diff/index.js index 5603586ae6..ae75d62dc9 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -309,10 +309,12 @@ export function diff( for (let i = excessDomChildren.length; i--; ) { removeNode(excessDomChildren[i]); } + markAsForce(newVNode); } } else { newVNode._dom = oldVNode._dom; newVNode._children = oldVNode._children; + if (!e.then) markAsForce(newVNode); } options._catchError(e, newVNode, oldVNode); } @@ -341,6 +343,11 @@ export function diff( return newVNode._flags & MODE_SUSPENDED ? undefined : oldDom; } +function markAsForce(vnode) { + if (vnode && vnode._component) vnode._component._force = true; + if (vnode && vnode._children) vnode._children.forEach(markAsForce); +} + /** * @param {Array} commitQueue List of components * which have callbacks to invoke in commitRoot