Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ssr): fix hydration mismatch warning about mutiple continuous tex… #7301

Closed
wants to merge 1 commit into from
Closed
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
19 changes: 19 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ describe('SSR hydration', () => {
expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
})

// #7285
test('element with multiple continuous text vnodes', async () => {
// should no mismatch warning
const { container } = mountWithHydration('<div>foo</div>', () =>
h('div', ['fo', 'o'])
)
expect(container.textContent).toBe('foo')
})

test('element with elements children', async () => {
const msg = ref('foo')
const fn = vi.fn()
Expand Down Expand Up @@ -224,6 +233,16 @@ describe('SSR hydration', () => {
)
})

// #7285
test('Fragment (multiple continuous text vnodes)', async () => {
// should no mismatch warning
const { container } = mountWithHydration('<!--[-->foo<!--]-->', () => [
'fo',
'o'
])
expect(container.textContent).toBe('foo')
})

test('Teleport', async () => {
const msg = ref('foo')
const fn = vi.fn()
Expand Down
73 changes: 70 additions & 3 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,78 @@ export function createHydrationFunctions(
optimized = optimized || !!parentVNode.dynamicChildren
const children = parentVNode.children as VNode[]
const l = children.length
const continuousTextVnodes = []
const getVnode = (i: number) =>
optimized ? children[i] : (children[i] = normalizeVNode(children[i]))
let hasWarned = false
for (let i = 0; i < l; i++) {
const vnode = optimized
? children[i]
: (children[i] = normalizeVNode(children[i]))
let vnode = getVnode(i)

// #7285 - multiple continuous text vnodes in children can cause hydration
// failure because the server rendered HTML just contain one text node
if (
vnode.type === Text &&
node &&
node.nodeType === DOMNodeTypes.TEXT &&
vnode.children !== (node as Text).data
) {
const nextVnode = getVnode(i + 1)
// for final merging into one text
continuousTextVnodes.push(vnode)
// if the next vnode is also text, it means the children has multiple continuous
// text vnodes, we need to merge them into one text to avoid hydration failure
if (nextVnode.type === Text) {
patch(
null,
vnode,
container,
node,
parentComponent,
parentSuspense,
isSVGContainer(container),
slotScopeIds
)
continue
} else if (continuousTextVnodes.length > 1) {
const text = continuousTextVnodes.map(v => v.children).join('')
if ((node as Text).data !== text) {
hasMismatch = true
__DEV__ &&
warn(
`Hydration text mismatch:` +
`\n- Client: ${JSON.stringify((node as Text).data)}` +
`\n- Server: ${text}`
)
}
// insert the last text vnode to the container
patch(
null,
vnode,
container,
node,
parentComponent,
parentSuspense,
isSVGContainer(container),
slotScopeIds
)

const nextNode = nextSibling(node)
// because the node's text has been inserted to the container,
// so we need to remove it
remove(node)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an error thrown by the mutation of the component's own state that spawns at renderer.ts line 1494 and it bubbles up until reaching the remove() function

Maybe that remove() function is trying to access the parentNode from the removed node here?

The error stack:

vue.runtime.esm-browser.js:9092 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'parentNode')
    at remove (vue.runtime.esm-browser.js:9092:26)
    at performRemove (vue.runtime.esm-browser.js:7636:7)
    at remove (vue.runtime.esm-browser.js:7650:7)
    at unmount (vue.runtime.esm-browser.js:7605:9)
    at patchKeyedChildren (vue.runtime.esm-browser.js:7402:9)
    at patchChildren (vue.runtime.esm-browser.js:7258:11)
    at patchElement (vue.runtime.esm-browser.js:6722:7)
    at processElement (vue.runtime.esm-browser.js:6557:7)
    at patch (vue.runtime.esm-browser.js:6418:11)
    at ReactiveEffect.componentUpdateFn [as fn] (vue.runtime.esm-browser.js:7152:9)

node = nextNode
vnode = nextVnode
continuousTextVnodes.length = 0
i++
if (i === l) {
continue
}
} else {
// if only one text vnode, do nothing
continuousTextVnodes.length = 0
}
}

if (node) {
node = hydrateNode(
node,
Expand Down