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