// Hydration: adopt server-rendered DOM instead of replacing it. // // Because the server and browser renderers serialize identically, morphing // the first client render onto the server DOM leaves matching nodes // untouched — the server DOM is adopted in place (same element references, // form state preserved) and only real differences mutate. What hydrate // adds over a bare morph is detection: before morphing it walks both trees // and reports the first place server and client markup disagree, which is // exactly the class of bug (Date.now() in views, user-specific content, // stale caches) that otherwise surfaces as a silent flash of changed // content. The client render always wins. import morph from './morph.js' export default function hydrate (oldNode, newNode, opts) { // The client tree can hold adjacent text nodes ("count is " + "0") // where the parsed server HTML has one merged run ("count is 0") — // identical serialization, different granularity. Fold the client tree // to parser granularity so comparison and morph see matching shapes. if (newNode.normalize) newNode.normalize() const onMismatch = opts && opts.onMismatch if (onMismatch) { const diff = firstDifference(oldNode, newNode, []) if (diff) onMismatch(diff) } return morph(oldNode, newNode) } export { hydrate } // Depth-first parallel walk; returns { path, reason, server, client } for // the first disagreement, or null when the trees agree. function firstDifference (a, b, path) { if (a.nodeType !== b.nodeType || a.nodeName !== b.nodeName) { return { path: pathString(path), reason: 'node', server: describe(a), client: describe(b) } } if (a.nodeType === 3 || a.nodeType === 8) { // text, comment // whitespace-insensitive: the parser shuffles insignificant // whitespace (e.g. text after reparents into body), and // morph reconciles it silently — only report meaningful text if (a.nodeValue.trim() !== b.nodeValue.trim()) { return { path: pathString(path), reason: 'text', server: a.nodeValue, client: b.nodeValue } } return null } if (a.nodeType === 1) { const attrDiff = attrDifference(a, b) if (attrDiff) { return { path: pathString(path.concat(a.nodeName.toLowerCase())), reason: 'attribute', server: attrDiff.server, client: attrDiff.client } } const aKids = significant(a.childNodes) const bKids = significant(b.childNodes) if (aKids.length !== bKids.length) { return { path: pathString(path.concat(a.nodeName.toLowerCase())), reason: 'children', server: aKids.length + ' child node(s)', client: bKids.length + ' child node(s)' } } for (let i = 0; i < aKids.length; i++) { const diff = firstDifference(aKids[i], bKids[i], path.concat(a.nodeName.toLowerCase())) if (diff) return diff } } return null } // child nodes that matter for comparison: everything except // whitespace-only text nodes and