diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js index b364cf41c6..010aa15832 100644 --- a/compat/test/browser/suspense-hydration.test.js +++ b/compat/test/browser/suspense-hydration.test.js @@ -97,6 +97,72 @@ describe('suspense hydration', () => { }); }); + it('Should hydrate a fragment with multiple children correctly', () => { + scratch.innerHTML = '
Hello
World!
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + + + , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '
Hello
World!
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(() => ( + <> +
Hello
+
World!
+ + )).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello
World!
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + + it('Should hydrate a fragment with no children correctly', () => { + scratch.innerHTML = '
Hello world
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + <> + + + +
Hello world
+ , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(() => null).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + it('should leave DOM untouched when suspending while hydrating', () => { scratch.innerHTML = '
Hello
'; clearLog(); diff --git a/mangle.json b/mangle.json index fcd2dce74f..2e67fca5a2 100644 --- a/mangle.json +++ b/mangle.json @@ -31,6 +31,7 @@ "$_list": "__", "$_pendingEffects": "__h", "$_value": "__", + "$_excess": "__x", "$_nextValue": "__N", "$_original": "__v", "$_args": "__H", diff --git a/src/diff/children.js b/src/diff/children.js index 57bbe89113..037064ccc7 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -127,6 +127,9 @@ export function diffChildren( oldDom = childVNode._nextDom; } else if (newDom) { oldDom = newDom.nextSibling; + while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { + oldDom = oldDom.nextSibling; + } } // Eagerly cleanup _nextDom. We don't need to persist the value because it diff --git a/src/diff/index.js b/src/diff/index.js index 84c801bd02..f677a347d5 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -52,8 +52,11 @@ export function diff( // If the previous diff bailed out, resume creating/hydrating. if (oldVNode._flags & MODE_SUSPENDED) { isHydrating = !!(oldVNode._flags & MODE_HYDRATE); - oldDom = newVNode._dom = oldVNode._dom; - excessDomChildren = [oldDom]; + excessDomChildren = oldVNode._component._excess; + // TODO: it's entirely possible for nested Suspense scenario's that we + // take another comment-node here as oldDom which isn't ideal however + // let's try it out for now. + oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0]; } if ((tmp = options._diff)) tmp(newVNode); @@ -277,10 +280,50 @@ export function diff( ? MODE_HYDRATE | MODE_SUSPENDED : MODE_HYDRATE; - while (oldDom && oldDom.nodeType === 8 && oldDom.nextSibling) { - oldDom = oldDom.nextSibling; + let shouldFallback = true, + commentMarkersToFind = 0, + done = false; + + newVNode._component._excess = []; + for (let i = 0; i < excessDomChildren.length; i++) { + let child = excessDomChildren[i]; + if (child == null || done) continue; + + // When we encounter a boundary with $s we are opening + // a boundary, this implies that we need to bump + // the amount of markers we need to find before closing + // the outer boundary. + // We exclude the open and closing marker from + // the future excessDomChildren but any nested one + // needs to be included for future suspensions. + if (child.nodeType == 8 && child.data == '$s') { + if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + } + commentMarkersToFind++; + shouldFallback = false; + excessDomChildren[i] = null; + } else if (child.nodeType == 8 && child.data == '/$s') { + commentMarkersToFind--; + if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + } + done = commentMarkersToFind === 0; + excessDomChildren[i] = null; + } else if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + excessDomChildren[i] = null; + } } - excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; + + if (shouldFallback) { + while (oldDom && oldDom.nodeType === 8 && oldDom.nextSibling) { + oldDom = oldDom.nextSibling; + } + excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; + newVNode._component._excess.push(oldDom); + } + newVNode._dom = oldDom; } else { newVNode._dom = oldVNode._dom; diff --git a/src/internal.d.ts b/src/internal.d.ts index cbf23b3888..e4c2461f16 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -162,6 +162,7 @@ declare global { state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks base?: PreactElement; + _excess: PreactElement[] | null; _dirty: boolean; _force?: boolean; _renderCallbacks: Array<() => void>; // Only class components