Skip to content

Commit

Permalink
Warn our user for hydration mismatches (#4490)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Sep 12, 2024
1 parent b976caa commit 022dbb1
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 4 deletions.
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

0 comments on commit 022dbb1

Please sign in to comment.