Skip to content

Commit 5e2be37

Browse files
committed
Warn our user for hydration mismatches
1 parent b976caa commit 5e2be37

File tree

5 files changed

+65
-4
lines changed

5 files changed

+65
-4
lines changed

debug/src/debug.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,3 +582,11 @@ export function serializeVNode(vnode) {
582582
children && children.length ? '>..</' + name + '>' : ' />'
583583
}`;
584584
}
585+
586+
options._hydrationMismatch = (newVNode, excessDomChildren) => {
587+
const { type } = newVNode;
588+
const availableTypes = excessDomChildren.map(child => child.localName);
589+
console.error(
590+
`Expected a DOM node of type ${type} but found ${availableTypes.join(', ')}as available DOM-node(s), this is caused by the SSR'd HTML containing different DOM-nodes compared to the hydrated one.\n\n${getOwnerStack(newVNode)}`
591+
);
592+
};

debug/test/browser/debug.test.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { createElement, render, createRef, Component, Fragment } from 'preact';
1+
import {
2+
createElement,
3+
render,
4+
createRef,
5+
Component,
6+
Fragment,
7+
hydrate
8+
} from 'preact';
29
import { useState } from 'preact/hooks';
310
import {
411
setupScratch,
@@ -870,4 +877,43 @@ describe('debug', () => {
870877
expect(console.error).to.not.be.called;
871878
});
872879
});
880+
881+
describe('Hydration mismatches', () => {
882+
it('Should warn us for a node mismatch', () => {
883+
scratch.innerHTML = '<div><span>foo</span>/div>';
884+
const App = () => (
885+
<div>
886+
<p>foo</p>
887+
</div>
888+
);
889+
hydrate(<App />, scratch);
890+
expect(console.error).to.be.calledOnce;
891+
expect(console.error).to.be.calledOnceWith(
892+
sinon.match(/Expected a DOM node of type p but found span/)
893+
);
894+
});
895+
896+
it('Should not warn for a text-node mismatch', () => {
897+
scratch.innerHTML = '<div>foo bar baz/div>';
898+
const App = () => (
899+
<div>
900+
foo {'bar'} {'baz'}
901+
</div>
902+
);
903+
hydrate(<App />, scratch);
904+
expect(console.error).to.not.be.called;
905+
});
906+
907+
it('Should not warn for a well-formed tree', () => {
908+
scratch.innerHTML = '<div><span>foo</span><span>bar</span></div>';
909+
const App = () => (
910+
<div>
911+
<span>foo</span>
912+
<span>bar</span>
913+
</div>
914+
);
915+
hydrate(<App />, scratch);
916+
expect(console.error).to.not.be.called;
917+
});
918+
});
873919
});

mangle.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"$_listeners": "l",
2929
"$_cleanup": "__c",
3030
"$__hooks": "__H",
31+
"$_hydrationMismatch": "__m",
3132
"$_list": "__",
3233
"$_pendingEffects": "__h",
3334
"$_value": "__",

src/diff/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,15 @@ function diffElementNodes(
417417
newProps.is && newProps
418418
);
419419

420-
// we created a new parent, so none of the previously attached children can be reused:
421-
excessDomChildren = null;
422420
// we are creating a new node, so we can assume this is a new subtree (in
423421
// case we are hydrating), this deopts the hydrate
424-
isHydrating = false;
422+
if (isHydrating) {
423+
if (options._hydrationMismatch)
424+
options._hydrationMismatch(newVNode, excessDomChildren);
425+
isHydrating = false;
426+
}
427+
// we created a new parent, so none of the previously attached children can be reused:
428+
excessDomChildren = null;
425429
}
426430

427431
if (nodeType === null) {

src/internal.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ declare global {
4545
oldVNode?: VNode | undefined,
4646
errorInfo?: ErrorInfo | undefined
4747
): void;
48+
/** Attach a hook that firs when hydration can't find a proper DOM-node to match with */
49+
_hydrationMismatch?(vnode: VNode, excessDomChildren: PreactElement[]): void;
4850
}
4951

5052
export type ComponentChild =

0 commit comments

Comments
 (0)