Skip to content

Commit ab2c308

Browse files
committed
wip: prop mismatch handling
1 parent e8a8117 commit ab2c308

File tree

5 files changed

+135
-54
lines changed

5 files changed

+135
-54
lines changed

packages/runtime-core/src/hydration.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -899,32 +899,42 @@ function propHasMismatch(
899899
}
900900

901901
if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) {
902-
const format = (v: any) =>
903-
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
904-
const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`
905-
const postSegment =
906-
`\n - rendered on server: ${format(actual)}` +
907-
`\n - expected on client: ${format(expected)}` +
908-
`\n Note: this mismatch is check-only. The DOM will not be rectified ` +
909-
`in production due to performance overhead.` +
910-
`\n You should fix the source of the mismatch.`
911-
if (__TEST__) {
912-
// during tests, log the full message in one single string for easier
913-
// debugging.
914-
warn(`${preSegment} ${el.tagName}${postSegment}`)
915-
} else {
916-
warn(preSegment, el, postSegment)
917-
}
902+
warnPropMismatch(el, mismatchKey, mismatchType, actual, expected)
918903
return true
919904
}
920905
return false
921906
}
922907

923-
function toClassSet(str: string): Set<string> {
908+
export function warnPropMismatch(
909+
el: Element & { $cls?: string },
910+
mismatchKey: string | undefined,
911+
mismatchType: MismatchTypes,
912+
actual: string | boolean | null | undefined,
913+
expected: string | boolean | null | undefined,
914+
): void {
915+
const format = (v: any) =>
916+
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
917+
const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`
918+
const postSegment =
919+
`\n - rendered on server: ${format(actual)}` +
920+
`\n - expected on client: ${format(expected)}` +
921+
`\n Note: this mismatch is check-only. The DOM will not be rectified ` +
922+
`in production due to performance overhead.` +
923+
`\n You should fix the source of the mismatch.`
924+
if (__TEST__) {
925+
// during tests, log the full message in one single string for easier
926+
// debugging.
927+
warn(`${preSegment} ${el.tagName}${postSegment}`)
928+
} else {
929+
warn(preSegment, el, postSegment)
930+
}
931+
}
932+
933+
export function toClassSet(str: string): Set<string> {
924934
return new Set(str.trim().split(/\s+/))
925935
}
926936

927-
function isSetEqual(a: Set<string>, b: Set<string>): boolean {
937+
export function isSetEqual(a: Set<string>, b: Set<string>): boolean {
928938
if (a.size !== b.size) {
929939
return false
930940
}

packages/runtime-core/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,4 +565,10 @@ export { createInternalObject } from './internalObject'
565565
/**
566566
* @internal
567567
*/
568-
export { MismatchTypes, isMismatchAllowed } from './hydration'
568+
export {
569+
MismatchTypes,
570+
isMismatchAllowed,
571+
toClassSet,
572+
isSetEqual,
573+
warnPropMismatch,
574+
} from './hydration'

packages/runtime-vapor/__tests__/hydration.spec.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,17 @@ describe('Vapor Mode hydration', () => {
30193019
expect(container.innerHTML).toBe('<div>bar</div>')
30203020
expect(`Hydration text content mismatch`).toHaveBeenWarned()
30213021
})
3022+
3023+
test('element with v-html', async () => {
3024+
const data = ref('<p>bar</p>')
3025+
const { container } = await mountWithHydration(
3026+
`<div><p>foo</p></div>`,
3027+
`<div v-html="data"></div>`,
3028+
data,
3029+
)
3030+
expect(container.innerHTML).toBe('<div><p>bar</p></div>')
3031+
expect(`Hydration children mismatch on`).toHaveBeenWarned()
3032+
})
30223033
// test('not enough children', () => {
30233034
// const { container } = mountWithHydration(`<div></div>`, () =>
30243035
// h('div', [h('span', 'foo'), h('span', 'bar')]),
@@ -3036,14 +3047,16 @@ describe('Vapor Mode hydration', () => {
30363047
// expect(container.innerHTML).toBe('<div><span>foo</span></div>')
30373048
// expect(`Hydration children mismatch`).toHaveBeenWarned()
30383049
// })
3039-
test.todo('complete mismatch', async () => {
3050+
test('complete mismatch', async () => {
30403051
const data = ref('span')
30413052
const { container } = await mountWithHydration(
30423053
`<div>foo</div><!--dynamic-component-->`,
30433054
`<component :is="data">foo</component>`,
30443055
data,
30453056
)
3046-
expect(container.innerHTML).toBe('<span>foo</span>')
3057+
expect(container.innerHTML).toBe(
3058+
'<span>foo</span><!--dynamic-component-->',
3059+
)
30473060
expect(`Hydration node mismatch`).toHaveBeenWarned()
30483061
})
30493062
// test('fragment mismatch removal', () => {
@@ -3102,30 +3115,30 @@ describe('Vapor Mode hydration', () => {
31023115
// expect(container.innerHTML).toBe('<div><!--hi--></div>')
31033116
// expect(`Hydration node mismatch`).toHaveBeenWarned()
31043117
// })
3105-
// test('class mismatch', () => {
3106-
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3107-
// h('div', { class: ['foo', 'bar'] }),
3108-
// )
3109-
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3110-
// h('div', { class: { foo: true, bar: true } }),
3111-
// )
3112-
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3113-
// h('div', { class: 'foo bar' }),
3114-
// )
3115-
// // SVG classes
3116-
// mountWithHydration(`<svg class="foo bar"></svg>`, () =>
3117-
// h('svg', { class: 'foo bar' }),
3118-
// )
3119-
// // class with different order
3120-
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3121-
// h('div', { class: 'bar foo' }),
3122-
// )
3123-
// expect(`Hydration class mismatch`).not.toHaveBeenWarned()
3124-
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3125-
// h('div', { class: 'foo' }),
3126-
// )
3127-
// expect(`Hydration class mismatch`).toHaveBeenWarned()
3128-
// })
3118+
test('class mismatch', () => {
3119+
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3120+
// h('div', { class: ['foo', 'bar'] }),
3121+
// )
3122+
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3123+
// h('div', { class: { foo: true, bar: true } }),
3124+
// )
3125+
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3126+
// h('div', { class: 'foo bar' }),
3127+
// )
3128+
// // SVG classes
3129+
// mountWithHydration(`<svg class="foo bar"></svg>`, () =>
3130+
// h('svg', { class: 'foo bar' }),
3131+
// )
3132+
// // class with different order
3133+
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3134+
// h('div', { class: 'bar foo' }),
3135+
// )
3136+
// expect(`Hydration class mismatch`).not.toHaveBeenWarned()
3137+
// mountWithHydration(`<div class="foo bar"></div>`, () =>
3138+
// h('div', { class: 'foo' }),
3139+
// )
3140+
// expect(`Hydration class mismatch`).toHaveBeenWarned()
3141+
})
31293142
// test('style mismatch', () => {
31303143
// mountWithHydration(`<div style="color:red;"></div>`, () =>
31313144
// h('div', { style: { color: 'red' } }),

packages/runtime-vapor/src/dom/hydration.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
parentNode,
1717
} from './node'
1818
import { BLOCK_ANCHOR_END_LABEL, BLOCK_ANCHOR_START_LABEL } from '@vue/shared'
19-
import { insert, remove } from '../block'
19+
import { remove } from '../block'
2020

2121
const isHydratingStack = [] as boolean[]
2222
export let isHydrating = false
@@ -269,15 +269,20 @@ function handleMismatch(node: Node, template: string): Node {
269269
const container = parentNode(node)!
270270
remove(node, container)
271271

272-
let newNode: Node | null
272+
// fast path for text nodes
273273
if (template[0] !== '<') {
274-
newNode = createTextNode(template)
275-
} else {
276-
const t = createElement('template') as HTMLTemplateElement
277-
t.innerHTML = template
278-
newNode = child(t.content).cloneNode(true)
274+
return container.insertBefore(createTextNode(template), next)
279275
}
280-
insert(newNode, container, next)
276+
277+
// element node
278+
const t = createElement('template') as HTMLTemplateElement
279+
t.innerHTML = template
280+
const newNode = child(t.content).cloneNode(true) as Element
281+
newNode.innerHTML = (node as Element).innerHTML
282+
Array.from((node as Element).attributes).forEach(attr => {
283+
newNode.setAttribute(attr.name, attr.value)
284+
})
285+
container.insertBefore(newNode, next)
281286
return newNode
282287
}
283288

packages/runtime-vapor/src/dom/prop.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import {
1313
MismatchTypes,
1414
currentInstance,
1515
isMismatchAllowed,
16+
isSetEqual,
1617
mergeProps,
1718
patchStyle,
1819
shouldSetAsProp,
20+
toClassSet,
1921
warn,
22+
warnPropMismatch,
2023
} from '@vue/runtime-dom'
2124
import {
2225
type VaporComponentInstance,
@@ -113,6 +116,31 @@ export function setDOMProp(el: any, key: string, value: any): void {
113116
}
114117

115118
export function setClass(el: TargetElement, value: any): void {
119+
if (isHydrating) {
120+
const actual = el.getAttribute('class')
121+
const actualClassSet = toClassSet(actual || '')
122+
let expected = normalizeClass(value)
123+
const expectedClassSet = toClassSet(expected)
124+
let hasMismatch = false
125+
if (el.$root) {
126+
hasMismatch = Array.from(expectedClassSet).some(
127+
cls => !actualClassSet.has(cls),
128+
)
129+
if (hasMismatch) {
130+
setClassIncremental(el, value)
131+
expected = el.getAttribute('class')!
132+
}
133+
}
134+
135+
if (hasMismatch || !isSetEqual(actualClassSet, expectedClassSet)) {
136+
warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
137+
if (!el.$root) el.className = expected
138+
}
139+
140+
if (!el.$root) el.$cls = expected
141+
return
142+
}
143+
116144
if (el.$root) {
117145
setClassIncremental(el, value)
118146
} else if ((value = normalizeClass(value)) !== el.$cls) {
@@ -192,7 +220,6 @@ export function setText(el: Text & { $txt?: string }, value: string): void {
192220
`\n - expected on client: ${JSON.stringify(value)}`,
193221
)
194222
logMismatchError()
195-
196223
el.nodeValue = value
197224
}
198225

@@ -248,6 +275,26 @@ export function setElementText(
248275

249276
export function setHtml(el: TargetElement, value: any): void {
250277
value = value == null ? '' : value
278+
279+
if (isHydrating) {
280+
if (el.innerHTML !== value) {
281+
if (!isMismatchAllowed(el, MismatchTypes.CHILDREN)) {
282+
if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
283+
warn(
284+
`Hydration children mismatch on`,
285+
el,
286+
`\nServer rendered element contains different child nodes from client nodes.`,
287+
)
288+
}
289+
logMismatchError()
290+
}
291+
el.innerHTML = value
292+
}
293+
294+
el.$html = value
295+
return
296+
}
297+
251298
if (el.$html !== value) {
252299
el.innerHTML = el.$html = value
253300
}

0 commit comments

Comments
 (0)