diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js index 9143903238..fd61a2bb02 100644 --- a/compat/test/browser/suspense-hydration.test.js +++ b/compat/test/browser/suspense-hydration.test.js @@ -97,6 +97,104 @@ 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( + + + , + 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(); + }); + }); + + // This is in theory correct but still it shows that our oldDom becomes stale very quickly + // and moves DOM into weird places + it.skip('Should hydrate a fragment with no children and an adjacent node correctly', () => { + scratch.innerHTML = '
Baz
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + <> + + + +
Baz
+ , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal('
Baz
'); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(() => null).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal('
Baz
'); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + it('should properly attach event listeners when suspending while hydrating', () => { scratch.innerHTML = '
Hello
World
'; clearLog(); diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index 454df6308a..98551fc28a 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -59,6 +59,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { _nextDom: undefined, _component: null, constructor: undefined, + _excess: null, _original: --vnodeId, _index: -1, _flags: 0, diff --git a/src/create-element.js b/src/create-element.js index 66898b2224..ebecdf4037 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -66,6 +66,7 @@ export function createVNode(type, props, key, ref, original) { _parent: null, _depth: 0, _dom: null, + _excess: null, // _nextDom must be initialized to undefined b/c it will eventually // be set to dom.nextSibling which can return `null` and it is important // to be able to distinguish between an uninitialized _nextDom and diff --git a/src/diff/index.js b/src/diff/index.js index 3e4b17bd62..25e02fe702 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -52,8 +52,13 @@ 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]; + if (oldVNode._excess) { + excessDomChildren = oldVNode._excess; + oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[1]; + } else { + oldDom = newVNode._dom = oldVNode._dom; + excessDomChildren = [oldDom]; + } } if ((tmp = options._diff)) tmp(newVNode); @@ -273,13 +278,36 @@ export function diff( newVNode._original = null; // if hydrating or creating initial tree, bailout preserves DOM: if (isHydrating || excessDomChildren != null) { - newVNode._dom = oldDom; newVNode._flags |= isHydrating ? MODE_HYDRATE | MODE_SUSPENDED : MODE_HYDRATE; - excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; - // ^ could possibly be simplified to: - // excessDomChildren.length = 0; + + let found = excessDomChildren.find( + child => child && child.nodeType == 8 && child.data == '$s' + ), + index = excessDomChildren.indexOf(found) + 1; + + newVNode._dom = oldDom; + if (found) { + let commentMarkersToFind = 1; + newVNode._excess = [found]; + excessDomChildren[index - 1] = null; + while (commentMarkersToFind && index <= excessDomChildren.length) { + const node = excessDomChildren[index]; + excessDomChildren[index] = null; + index++; + newVNode._excess.push(node); + if (node.nodeType == 8) { + if (node.data == '$s') { + commentMarkersToFind++; + } else if (node.data == '/$s') { + commentMarkersToFind--; + } + } + } + } else { + excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; + } } else { newVNode._dom = oldVNode._dom; newVNode._children = oldVNode._children; diff --git a/src/internal.d.ts b/src/internal.d.ts index cbf23b3888..576a6ca523 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -145,6 +145,7 @@ declare global { * The [first (for Fragments)] DOM child of a VNode */ _dom: PreactElement | null; + _excess: PreactElement[] | null; /** * The last dom child of a Fragment, or components that return a Fragment */