Skip to content

Commit 91269da

Browse files
committed
feat(ssr): hydration mismatch handling
1 parent 7971b04 commit 91269da

File tree

1 file changed

+125
-11
lines changed

1 file changed

+125
-11
lines changed

packages/runtime-core/src/hydration.ts

+125-11
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,86 @@ export type RootHydrateFunction = (
1717
container: Element
1818
) => void
1919

20+
const enum DOMNodeTypes {
21+
ELEMENT = 1,
22+
TEXT = 3,
23+
COMMENT = 8
24+
}
25+
26+
let hasHydrationMismatch = false
27+
2028
// Note: hydration is DOM-specific
2129
// But we have to place it in core due to tight coupling with core - splitting
2230
// it out creates a ton of unnecessary complexity.
2331
// Hydration also depends on some renderer internal logic which needs to be
2432
// passed in via arguments.
2533
export function createHydrationFunctions({
2634
mt: mountComponent,
35+
p: patch,
2736
o: { patchProp, createText }
2837
}: RendererInternals<Node, Element>) {
2938
const hydrate: RootHydrateFunction = (vnode, container) => {
3039
if (__DEV__ && !container.hasChildNodes()) {
31-
warn(`Attempting to hydrate existing markup but container is empty.`)
40+
warn(
41+
`Attempting to hydrate existing markup but container is empty. ` +
42+
`Performing full mount instead.`
43+
)
44+
patch(null, vnode, container)
3245
return
3346
}
47+
hasHydrationMismatch = false
3448
hydrateNode(container.firstChild!, vnode)
3549
flushPostFlushCbs()
50+
if (hasHydrationMismatch) {
51+
// this error should show up in production
52+
console.error(`Hydration completed but contains mismatches.`)
53+
}
3654
}
3755

38-
// TODO handle mismatches
3956
const hydrateNode = (
4057
node: Node,
4158
vnode: VNode,
4259
parentComponent: ComponentInternalInstance | null = null,
4360
optimized = false
4461
): Node | null => {
4562
const { type, shapeFlag } = vnode
63+
const domType = node.nodeType
64+
4665
vnode.el = node
66+
4767
switch (type) {
4868
case Text:
69+
if (domType !== DOMNodeTypes.TEXT) {
70+
return handleMismtach(node, vnode, parentComponent)
71+
}
72+
if ((node as Text).data !== vnode.children) {
73+
hasHydrationMismatch = true
74+
__DEV__ &&
75+
warn(
76+
`Hydration text mismatch:` +
77+
`\n- Client: ${JSON.stringify(vnode.children)}`,
78+
`\n- Server: ${JSON.stringify((node as Text).data)}`
79+
)
80+
;(node as Text).data = vnode.children as string
81+
}
82+
return node.nextSibling
4983
case Comment:
84+
if (domType !== DOMNodeTypes.COMMENT) {
85+
return handleMismtach(node, vnode, parentComponent)
86+
}
87+
return node.nextSibling
5088
case Static:
89+
if (domType !== DOMNodeTypes.ELEMENT) {
90+
return handleMismtach(node, vnode, parentComponent)
91+
}
5192
return node.nextSibling
5293
case Fragment:
5394
return hydrateFragment(node, vnode, parentComponent, optimized)
5495
default:
5596
if (shapeFlag & ShapeFlags.ELEMENT) {
97+
if (domType !== DOMNodeTypes.ELEMENT) {
98+
return handleMismtach(node, vnode, parentComponent)
99+
}
56100
return hydrateElement(
57101
node as Element,
58102
vnode,
@@ -67,7 +111,15 @@ export function createHydrationFunctions({
67111
const subTree = vnode.component!.subTree
68112
return (subTree.anchor || subTree.el).nextSibling
69113
} else if (shapeFlag & ShapeFlags.PORTAL) {
70-
hydratePortal(vnode, parentComponent, optimized)
114+
if (domType !== DOMNodeTypes.COMMENT) {
115+
return handleMismtach(node, vnode, parentComponent)
116+
}
117+
hydratePortal(
118+
vnode,
119+
node.parentNode as Element,
120+
parentComponent,
121+
optimized
122+
)
71123
return node.nextSibling
72124
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
73125
// TODO Suspense
@@ -84,7 +136,7 @@ export function createHydrationFunctions({
84136
parentComponent: ComponentInternalInstance | null,
85137
optimized: boolean
86138
) => {
87-
const { props, patchFlag } = vnode
139+
const { props, patchFlag, shapeFlag } = vnode
88140
// skip props & children if this is hoisted static nodes
89141
if (patchFlag !== PatchFlags.HOISTED) {
90142
// props
@@ -116,16 +168,31 @@ export function createHydrationFunctions({
116168
}
117169
// children
118170
if (
119-
vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
171+
shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
120172
// skip if element has innerHTML / textContent
121173
!(props !== null && (props.innerHTML || props.textContent))
122174
) {
123-
hydrateChildren(
175+
let next = hydrateChildren(
124176
el.firstChild,
125177
vnode,
178+
el,
126179
parentComponent,
127180
optimized || vnode.dynamicChildren !== null
128181
)
182+
while (next) {
183+
hasHydrationMismatch = true
184+
__DEV__ &&
185+
warn(
186+
`Hydration children mismatch: ` +
187+
`server rendered element contains more child nodes than client vdom.`
188+
)
189+
// The SSRed DOM contains more nodes than it should. Remove them.
190+
const cur = next
191+
next = next.nextSibling
192+
el.removeChild(cur)
193+
}
194+
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
195+
el.textContent = vnode.children as string
129196
}
130197
}
131198
return el.nextSibling
@@ -134,16 +201,28 @@ export function createHydrationFunctions({
134201
const hydrateChildren = (
135202
node: Node | null,
136203
vnode: VNode,
204+
container: Element,
137205
parentComponent: ComponentInternalInstance | null,
138206
optimized: boolean
139207
): Node | null => {
140208
const children = vnode.children as VNode[]
141209
optimized = optimized || vnode.dynamicChildren !== null
142-
for (let i = 0; node != null && i < children.length; i++) {
210+
for (let i = 0; i < children.length; i++) {
143211
const vnode = optimized
144212
? children[i]
145213
: (children[i] = normalizeVNode(children[i]))
146-
node = hydrateNode(node, vnode, parentComponent, optimized)
214+
if (node) {
215+
node = hydrateNode(node, vnode, parentComponent, optimized)
216+
} else {
217+
hasHydrationMismatch = true
218+
__DEV__ &&
219+
warn(
220+
`Hydration children mismatch: ` +
221+
`server rendered element contains fewer child nodes than client vdom.`
222+
)
223+
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
224+
patch(null, vnode, container)
225+
}
147226
}
148227
return node
149228
}
@@ -154,15 +233,22 @@ export function createHydrationFunctions({
154233
parentComponent: ComponentInternalInstance | null,
155234
optimized: boolean
156235
) => {
157-
const parent = node.parentNode!
236+
const parent = node.parentNode as Element
158237
parent.insertBefore((vnode.el = createText('')), node)
159-
const next = hydrateChildren(node, vnode, parentComponent, optimized)
238+
const next = hydrateChildren(
239+
node,
240+
vnode,
241+
parent,
242+
parentComponent,
243+
optimized
244+
)
160245
parent.insertBefore((vnode.anchor = createText('')), next)
161246
return next
162247
}
163248

164249
const hydratePortal = (
165250
vnode: VNode,
251+
container: Element,
166252
parentComponent: ComponentInternalInstance | null,
167253
optimized: boolean
168254
) => {
@@ -171,9 +257,37 @@ export function createHydrationFunctions({
171257
? document.querySelector(targetSelector)
172258
: targetSelector)
173259
if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
174-
hydrateChildren(target.firstChild, vnode, parentComponent, optimized)
260+
hydrateChildren(
261+
target.firstChild,
262+
vnode,
263+
container,
264+
parentComponent,
265+
optimized
266+
)
175267
}
176268
}
177269

270+
const handleMismtach = (
271+
node: Node,
272+
vnode: VNode,
273+
parentComponent: ComponentInternalInstance | null
274+
) => {
275+
hasHydrationMismatch = true
276+
__DEV__ &&
277+
warn(
278+
`Hydration node mismatch:\n- Client vnode:`,
279+
vnode.type,
280+
`\n- Server rendered DOM:`,
281+
node
282+
)
283+
vnode.el = null
284+
const next = node.nextSibling
285+
const container = node.parentNode as Element
286+
container.removeChild(node)
287+
// TODO Suspense and SVG
288+
patch(null, vnode, container, next, parentComponent)
289+
return next
290+
}
291+
178292
return [hydrate, hydrateNode] as const
179293
}

0 commit comments

Comments
 (0)