Skip to content

Commit 92379cb

Browse files
committed
Comment denoted hydration
1 parent 13014a5 commit 92379cb

File tree

4 files changed

+232
-9
lines changed

4 files changed

+232
-9
lines changed

compat/test/browser/suspense-hydration.test.js

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -774,10 +774,18 @@ describe('suspense hydration', () => {
774774
});
775775
});
776776

777-
// Currently not supported, but I wrote the test before I realized that so
778-
// leaving it here in case we do support it eventually
777+
// TODO: this should start workign now but still bugged
779778
it.skip('should properly hydrate suspense when resolves to a Fragment', () => {
780-
const originalHtml = ul([li(0), li(1), li(2), li(3), li(4), li(5)]);
779+
const originalHtml = ul([
780+
li(0),
781+
li(1),
782+
'<!--$s-->',
783+
li(2),
784+
li(3),
785+
'<!--/$s-->',
786+
li(4),
787+
li(5)
788+
]);
781789

782790
const listeners = [
783791
sinon.spy(),
@@ -809,8 +817,8 @@ describe('suspense hydration', () => {
809817
scratch
810818
);
811819
rerender(); // Flush rerender queue to mimic what preact will really do
812-
expect(scratch.innerHTML).to.equal(originalHtml);
813820
expect(getLog()).to.deep.equal([]);
821+
expect(scratch.innerHTML).to.equal(originalHtml);
814822
expect(listeners[5]).not.to.have.been.called;
815823

816824
clearLog();
@@ -839,4 +847,169 @@ describe('suspense hydration', () => {
839847
expect(listeners[5]).to.have.been.calledTwice;
840848
});
841849
});
850+
851+
it('Should hydrate a fragment with multiple children correctly', () => {
852+
scratch.innerHTML = '<!--$s--><div>Hello</div><div>World!</div><!--/$s-->';
853+
clearLog();
854+
855+
const [Lazy, resolve] = createLazy();
856+
hydrate(
857+
<Suspense>
858+
<Lazy />
859+
</Suspense>,
860+
scratch
861+
);
862+
rerender(); // Flush rerender queue to mimic what preact will really do
863+
expect(scratch.innerHTML).to.equal(
864+
'<!--$s--><div>Hello</div><div>World!</div><!--/$s-->'
865+
);
866+
expect(getLog()).to.deep.equal([]);
867+
clearLog();
868+
869+
return resolve(() => (
870+
<>
871+
<div>Hello</div>
872+
<div>World!</div>
873+
</>
874+
)).then(() => {
875+
rerender();
876+
expect(scratch.innerHTML).to.equal(
877+
'<!--$s--><div>Hello</div><div>World!</div><!--/$s-->'
878+
);
879+
expect(getLog()).to.deep.equal([]);
880+
881+
clearLog();
882+
});
883+
});
884+
885+
it('Should hydrate a fragment with no children correctly', () => {
886+
scratch.innerHTML = '<!--$s--><!--/$s--><div>Hello world</div>';
887+
clearLog();
888+
889+
const [Lazy, resolve] = createLazy();
890+
hydrate(
891+
<>
892+
<Suspense>
893+
<Lazy />
894+
</Suspense>
895+
<div>Hello world</div>
896+
</>,
897+
scratch
898+
);
899+
rerender(); // Flush rerender queue to mimic what preact will really do
900+
expect(scratch.innerHTML).to.equal(
901+
'<!--$s--><!--/$s--><div>Hello world</div>'
902+
);
903+
expect(getLog()).to.deep.equal([]);
904+
clearLog();
905+
906+
return resolve(() => null).then(() => {
907+
rerender();
908+
expect(scratch.innerHTML).to.equal(
909+
'<!--$s--><!--/$s--><div>Hello world</div>'
910+
);
911+
expect(getLog()).to.deep.equal([]);
912+
913+
clearLog();
914+
});
915+
});
916+
917+
it('Should hydrate a fragment with no children correctly deeply', () => {
918+
scratch.innerHTML =
919+
'<!--$s--><!--$s--><!--/$s--><!--/$s--><div>Hello world</div>';
920+
clearLog();
921+
922+
const [Lazy, resolve] = createLazy();
923+
const [Lazy2, resolve2] = createLazy();
924+
hydrate(
925+
<>
926+
<Suspense>
927+
<Lazy>
928+
<Suspense>
929+
<Lazy2 />
930+
</Suspense>
931+
</Lazy>
932+
</Suspense>
933+
<div>Hello world</div>
934+
</>,
935+
scratch
936+
);
937+
rerender(); // Flush rerender queue to mimic what preact will really do
938+
expect(scratch.innerHTML).to.equal(
939+
'<!--$s--><!--$s--><!--/$s--><!--/$s--><div>Hello world</div>'
940+
);
941+
expect(getLog()).to.deep.equal([]);
942+
clearLog();
943+
944+
return resolve(p => p.children).then(() => {
945+
rerender();
946+
expect(scratch.innerHTML).to.equal(
947+
'<!--$s--><!--$s--><!--/$s--><!--/$s--><div>Hello world</div>'
948+
);
949+
expect(getLog()).to.deep.equal([]);
950+
951+
clearLog();
952+
return resolve2(() => null).then(() => {
953+
rerender();
954+
expect(scratch.innerHTML).to.equal(
955+
'<!--$s--><!--$s--><!--/$s--><!--/$s--><div>Hello world</div>'
956+
);
957+
expect(getLog()).to.deep.equal([]);
958+
959+
clearLog();
960+
});
961+
});
962+
});
963+
964+
it('Should hydrate a fragment with multiple children correctly deeply', () => {
965+
scratch.innerHTML =
966+
'<!--$s--><!--$s--><p>I am</p><span>Fragment</span><!--/$s--><!--/$s--><div>Hello world</div>';
967+
clearLog();
968+
969+
const [Lazy, resolve] = createLazy();
970+
const [Lazy2, resolve2] = createLazy();
971+
hydrate(
972+
<>
973+
<Suspense>
974+
<Lazy>
975+
<Suspense>
976+
<Lazy2 />
977+
</Suspense>
978+
</Lazy>
979+
</Suspense>
980+
<div>Hello world</div>
981+
</>,
982+
scratch
983+
);
984+
rerender(); // Flush rerender queue to mimic what preact will really do
985+
expect(scratch.innerHTML).to.equal(
986+
'<!--$s--><!--$s--><p>I am</p><span>Fragment</span><!--/$s--><!--/$s--><div>Hello world</div>'
987+
);
988+
expect(getLog()).to.deep.equal([]);
989+
clearLog();
990+
991+
return resolve(p => p.children).then(() => {
992+
rerender();
993+
expect(scratch.innerHTML).to.equal(
994+
'<!--$s--><!--$s--><p>I am</p><span>Fragment</span><!--/$s--><!--/$s--><div>Hello world</div>'
995+
);
996+
expect(getLog()).to.deep.equal([]);
997+
998+
clearLog();
999+
return resolve2(() => (
1000+
<>
1001+
<p>I am</p>
1002+
<span>Fragment</span>
1003+
</>
1004+
)).then(() => {
1005+
rerender();
1006+
expect(scratch.innerHTML).to.equal(
1007+
'<!--$s--><!--$s--><p>I am</p><span>Fragment</span><!--/$s--><!--/$s--><div>Hello world</div>'
1008+
);
1009+
expect(getLog()).to.deep.equal([]);
1010+
1011+
clearLog();
1012+
});
1013+
});
1014+
});
8421015
});

src/diff/children.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ export function diffChildren(
130130
oldDom = insert(childVNode, oldDom, parentDom);
131131
} else if (typeof childVNode.type == 'function' && result !== UNDEFINED) {
132132
oldDom = result;
133+
let toResolve = 0;
134+
while ((oldDom && oldDom.nodeType == 8) || toResolve > 0) {
135+
if (oldDom && oldDom.nodeType == 8 && oldDom.data == '$s') {
136+
toResolve++;
137+
} else if (oldDom && oldDom.nodeType == 8 && oldDom.data == '/$s') {
138+
toResolve--;
139+
}
140+
oldDom = oldDom.nextSibling;
141+
}
133142
} else if (newDom) {
134143
oldDom = newDom.nextSibling;
135144
}

src/diff/index.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export function diff(
6868
// If the previous diff bailed out, resume creating/hydrating.
6969
if (oldVNode._flags & MODE_SUSPENDED) {
7070
isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
71-
oldDom = newVNode._dom = oldVNode._dom;
72-
excessDomChildren = [oldDom];
71+
excessDomChildren = oldVNode._component._excess;
72+
oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0];
7373
}
7474

7575
if ((tmp = options._diff)) tmp(newVNode);
@@ -288,15 +288,55 @@ export function diff(
288288
// if hydrating or creating initial tree, bailout preserves DOM:
289289
if (isHydrating || excessDomChildren != null) {
290290
if (e.then) {
291+
let shouldFallback = true,
292+
commentMarkersToFind = 0,
293+
done = false;
294+
291295
newVNode._flags |= isHydrating
292296
? MODE_HYDRATE | MODE_SUSPENDED
293297
: MODE_SUSPENDED;
294298

295-
while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
296-
oldDom = oldDom.nextSibling;
299+
newVNode._component._excess = [];
300+
for (let i = 0; i < excessDomChildren.length; i++) {
301+
let child = excessDomChildren[i];
302+
if (child == null || done) continue;
303+
304+
// When we encounter a boundary with $s we are opening
305+
// a boundary, this implies that we need to bump
306+
// the amount of markers we need to find before closing
307+
// the outer boundary.
308+
// We exclude the open and closing marker from
309+
// the future excessDomChildren but any nested one
310+
// needs to be included for future suspensions.
311+
if (child.nodeType == 8 && child.data == '$s') {
312+
if (commentMarkersToFind > 0) {
313+
newVNode._component._excess.push(child);
314+
}
315+
commentMarkersToFind++;
316+
shouldFallback = false;
317+
excessDomChildren[i] = null;
318+
} else if (child.nodeType == 8 && child.data == '/$s') {
319+
commentMarkersToFind--;
320+
if (commentMarkersToFind > 0) {
321+
newVNode._component._excess.push(child);
322+
}
323+
done = commentMarkersToFind === 0;
324+
excessDomChildren[i] = null;
325+
} else if (commentMarkersToFind > 0) {
326+
newVNode._component._excess.push(child);
327+
excessDomChildren[i] = null;
328+
}
329+
}
330+
331+
if (shouldFallback) {
332+
while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
333+
oldDom = oldDom.nextSibling;
334+
}
335+
336+
excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
337+
newVNode._component._excess.push(oldDom);
297338
}
298339

299-
excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
300340
newVNode._dom = oldDom;
301341
} else {
302342
for (let i = excessDomChildren.length; i--; ) {

src/internal.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export interface Component<P = {}, S = {}> extends preact.Component<P, S> {
162162
constructor: ComponentType<P>;
163163
state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks
164164

165+
_excess?: PreactElement[];
165166
_dirty: boolean;
166167
_force?: boolean;
167168
_renderCallbacks: Array<() => void>; // Only class components

0 commit comments

Comments
 (0)