Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions debug/src/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,11 @@ export function serializeVNode(vnode) {
children && children.length ? '>..</' + name + '>' : ' />'
}`;
}

options._hydrationMismatch = (newVNode, excessDomChildren) => {
const { type } = newVNode;
const availableTypes = excessDomChildren.map(child => child.localName);
console.error(
`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)}`
);
};
48 changes: 47 additions & 1 deletion debug/test/browser/debug.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { createElement, render, createRef, Component, Fragment } from 'preact';
import {
createElement,
render,
createRef,
Component,
Fragment,
hydrate
} from 'preact';
import { useState } from 'preact/hooks';
import {
setupScratch,
Expand Down Expand Up @@ -870,4 +877,43 @@ describe('debug', () => {
expect(console.error).to.not.be.called;
});
});

describe('Hydration mismatches', () => {
it('Should warn us for a node mismatch', () => {
scratch.innerHTML = '<div><span>foo</span>/div>';
const App = () => (
<div>
<p>foo</p>
</div>
);
hydrate(<App />, scratch);
expect(console.error).to.be.calledOnce;
expect(console.error).to.be.calledOnceWith(
sinon.match(/Expected a DOM node of type p but found span/)
);
});

it('Should not warn for a text-node mismatch', () => {
scratch.innerHTML = '<div>foo bar baz/div>';
const App = () => (
<div>
foo {'bar'} {'baz'}
</div>
);
hydrate(<App />, scratch);
expect(console.error).to.not.be.called;
});

it('Should not warn for a well-formed tree', () => {
scratch.innerHTML = '<div><span>foo</span><span>bar</span></div>';
const App = () => (
<div>
<span>foo</span>
<span>bar</span>
</div>
);
hydrate(<App />, scratch);
expect(console.error).to.not.be.called;
});
});
});
1 change: 1 addition & 0 deletions mangle.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"$_listeners": "l",
"$_cleanup": "__c",
"$__hooks": "__H",
"$_hydrationMismatch": "__m",
"$_list": "__",
"$_pendingEffects": "__h",
"$_value": "__",
Expand Down
10 changes: 7 additions & 3 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,15 @@ function diffElementNodes(
newProps.is && newProps
);

// we created a new parent, so none of the previously attached children can be reused:
excessDomChildren = null;
// we are creating a new node, so we can assume this is a new subtree (in
// case we are hydrating), this deopts the hydrate
isHydrating = false;
if (isHydrating) {
if (options._hydrationMismatch)
options._hydrationMismatch(newVNode, excessDomChildren);
isHydrating = false;
}
// we created a new parent, so none of the previously attached children can be reused:
excessDomChildren = null;
}

if (nodeType === null) {
Expand Down
2 changes: 2 additions & 0 deletions src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ declare global {
oldVNode?: VNode | undefined,
errorInfo?: ErrorInfo | undefined
): void;
/** Attach a hook that firs when hydration can't find a proper DOM-node to match with */
_hydrationMismatch?(vnode: VNode, excessDomChildren: PreactElement[]): void;
}

export type ComponentChild =
Expand Down