Skip to content

Commit 7b18cdb

Browse files
committed
fix(teleport/ssr): fix Teleport hydration regression due to targetStart anchor addition
1 parent 12667da commit 7b18cdb

File tree

4 files changed

+114
-40
lines changed

4 files changed

+114
-40
lines changed

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

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ describe('SSR hydration', () => {
265265
const fn = vi.fn()
266266
const teleportContainer = document.createElement('div')
267267
teleportContainer.id = 'teleport'
268-
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
268+
teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
269269
document.body.appendChild(teleportContainer)
270270

271271
const { vnode, container } = mountWithHydration(
@@ -281,13 +281,14 @@ describe('SSR hydration', () => {
281281
expect(vnode.anchor).toBe(container.lastChild)
282282

283283
expect(vnode.target).toBe(teleportContainer)
284+
expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
284285
expect((vnode.children as VNode[])[0].el).toBe(
285-
teleportContainer.childNodes[0],
286+
teleportContainer.childNodes[1],
286287
)
287288
expect((vnode.children as VNode[])[1].el).toBe(
288-
teleportContainer.childNodes[1],
289+
teleportContainer.childNodes[2],
289290
)
290-
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
291+
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
291292

292293
// event handler
293294
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@@ -296,7 +297,7 @@ describe('SSR hydration', () => {
296297
msg.value = 'bar'
297298
await nextTick()
298299
expect(teleportContainer.innerHTML).toBe(
299-
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
300+
`<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
300301
)
301302
})
302303

@@ -326,7 +327,7 @@ describe('SSR hydration', () => {
326327

327328
const teleportHtml = ctx.teleports!['#teleport2']
328329
expect(teleportHtml).toMatchInlineSnapshot(
329-
`"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
330+
`"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
330331
)
331332

332333
teleportContainer.innerHTML = teleportHtml
@@ -342,16 +343,18 @@ describe('SSR hydration', () => {
342343
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
343344

344345
expect(teleportVnode1.target).toBe(teleportContainer)
346+
expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
345347
expect((teleportVnode1 as any).children[0].el).toBe(
346-
teleportContainer.childNodes[0],
348+
teleportContainer.childNodes[1],
347349
)
348-
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
350+
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
349351

350352
expect(teleportVnode2.target).toBe(teleportContainer)
353+
expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
351354
expect((teleportVnode2 as any).children[0].el).toBe(
352-
teleportContainer.childNodes[3],
355+
teleportContainer.childNodes[5],
353356
)
354-
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
357+
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
355358

356359
// // event handler
357360
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@@ -363,7 +366,7 @@ describe('SSR hydration', () => {
363366
msg.value = 'bar'
364367
await nextTick()
365368
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
366-
`"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
369+
`"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
367370
)
368371
})
369372

@@ -390,7 +393,9 @@ describe('SSR hydration', () => {
390393
)
391394

392395
const teleportHtml = ctx.teleports!['#teleport3']
393-
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
396+
expect(teleportHtml).toMatchInlineSnapshot(
397+
`"<!--teleport start anchor--><!--teleport anchor-->"`,
398+
)
394399

395400
teleportContainer.innerHTML = teleportHtml
396401
document.body.appendChild(teleportContainer)
@@ -413,7 +418,8 @@ describe('SSR hydration', () => {
413418
expect(children[2].el).toBe(container.childNodes[6])
414419

415420
expect(teleportVnode.target).toBe(teleportContainer)
416-
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
421+
expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
422+
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
417423

418424
// // event handler
419425
triggerEvent('click', container.querySelector('.foo')!)
@@ -454,7 +460,7 @@ describe('SSR hydration', () => {
454460
test('Teleport (as component root)', () => {
455461
const teleportContainer = document.createElement('div')
456462
teleportContainer.id = 'teleport4'
457-
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
463+
teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
458464
document.body.appendChild(teleportContainer)
459465

460466
const wrapper = {
@@ -483,7 +489,7 @@ describe('SSR hydration', () => {
483489
test('Teleport (nested)', () => {
484490
const teleportContainer = document.createElement('div')
485491
teleportContainer.id = 'teleport5'
486-
teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
492+
teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
487493
document.body.appendChild(teleportContainer)
488494

489495
const { vnode, container } = mountWithHydration(
@@ -498,7 +504,7 @@ describe('SSR hydration', () => {
498504
expect(vnode.anchor).toBe(container.lastChild)
499505

500506
const childDivVNode = (vnode as any).children[0]
501-
const div = teleportContainer.firstChild
507+
const div = teleportContainer.childNodes[1]
502508
expect(childDivVNode.el).toBe(div)
503509
expect(vnode.targetAnchor).toBe(div?.nextSibling)
504510

@@ -548,6 +554,66 @@ describe('SSR hydration', () => {
548554
teleportContainer.id = 'target'
549555
document.body.appendChild(teleportContainer)
550556

557+
// server render
558+
const ctx: SSRContext = {}
559+
container.innerHTML = await renderToString(h(App), ctx)
560+
expect(container.innerHTML).toBe(
561+
'<div><!--teleport start--><!--teleport end--></div>',
562+
)
563+
teleportContainer.innerHTML = ctx.teleports!['#target']
564+
565+
// hydrate
566+
createSSRApp(App).mount(container)
567+
expect(container.innerHTML).toBe(
568+
'<div><!--teleport start--><!--teleport end--></div>',
569+
)
570+
expect(teleportContainer.innerHTML).toBe(
571+
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
572+
)
573+
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
574+
575+
toggle.value = false
576+
await nextTick()
577+
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
578+
expect(teleportContainer.innerHTML).toBe('')
579+
})
580+
581+
test('Teleport unmount (mismatch + full integration)', async () => {
582+
const Comp1 = {
583+
template: `
584+
<Teleport to="#target">
585+
<span>Teleported Comp1</span>
586+
</Teleport>
587+
`,
588+
}
589+
const Comp2 = {
590+
template: `
591+
<div>Comp2</div>
592+
`,
593+
}
594+
595+
const toggle = ref(true)
596+
const App = {
597+
template: `
598+
<div>
599+
<Comp1 v-if="toggle"/>
600+
<Comp2 v-else/>
601+
</div>
602+
`,
603+
components: {
604+
Comp1,
605+
Comp2,
606+
},
607+
setup() {
608+
return { toggle }
609+
},
610+
}
611+
612+
const container = document.createElement('div')
613+
const teleportContainer = document.createElement('div')
614+
teleportContainer.id = 'target'
615+
document.body.appendChild(teleportContainer)
616+
551617
// server render
552618
container.innerHTML = await renderToString(h(App))
553619
expect(container.innerHTML).toBe(
@@ -569,7 +635,7 @@ describe('SSR hydration', () => {
569635
expect(teleportContainer.innerHTML).toBe('')
570636
})
571637

572-
test('Teleport target change (full integration)', async () => {
638+
test('Teleport target change (mismatch + full integration)', async () => {
573639
const target = ref('#target1')
574640
const Comp = {
575641
template: `

packages/runtime-core/src/components/Teleport.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ function hydrateTeleport(
381381
slotScopeIds,
382382
optimized,
383383
)
384-
vnode.targetStart = vnode.targetAnchor = targetNode
384+
vnode.targetStart = targetNode
385+
vnode.targetAnchor = targetNode && nextSibling(targetNode)
385386
} else {
386387
vnode.anchor = nextSibling(node)
387388

@@ -390,28 +391,29 @@ function hydrateTeleport(
390391
// could be nested teleports
391392
let targetAnchor = targetNode
392393
while (targetAnchor) {
393-
targetAnchor = nextSibling(targetAnchor)
394-
if (
395-
targetAnchor &&
396-
targetAnchor.nodeType === 8 &&
397-
(targetAnchor as Comment).data === 'teleport anchor'
398-
) {
399-
vnode.targetAnchor = targetAnchor
400-
;(target as TeleportTargetElement)._lpa =
401-
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
402-
break
394+
if (targetAnchor && targetAnchor.nodeType === 8) {
395+
if ((targetAnchor as Comment).data === 'teleport start anchor') {
396+
vnode.targetStart = targetAnchor
397+
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
398+
vnode.targetAnchor = targetAnchor
399+
;(target as TeleportTargetElement)._lpa =
400+
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
401+
break
402+
}
403403
}
404+
targetAnchor = nextSibling(targetAnchor)
404405
}
405406

406-
// #11400 if the HTML corresponding to Teleport is not embedded in the correct position
407-
// on the final page during SSR. the targetAnchor will always be null, we need to
408-
// manually add targetAnchor to ensure Teleport it can properly unmount or move
407+
// #11400 if the HTML corresponding to Teleport is not embedded in the
408+
// correct position on the final page during SSR. the targetAnchor will
409+
// always be null, we need to manually add targetAnchor to ensure
410+
// Teleport it can properly unmount or move
409411
if (!vnode.targetAnchor) {
410412
prepareAnchor(target, vnode, createText, insert)
411413
}
412414

413415
hydrateChildren(
414-
targetNode,
416+
targetNode && nextSibling(targetNode),
415417
vnode,
416418
target,
417419
parentComponent,

packages/server-renderer/__tests__/ssrTeleport.spec.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
2828
)
2929
expect(html).toBe('<!--teleport start--><!--teleport end-->')
3030
expect(ctx.teleports!['#target']).toBe(
31-
`<div>content</div><!--teleport anchor-->`,
31+
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
3232
)
3333
})
3434

@@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => {
5656
expect(html).toBe(
5757
'<!--teleport start--><div>content</div><!--teleport end-->',
5858
)
59-
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
59+
expect(ctx.teleports!['#target']).toBe(
60+
`<!--teleport start anchor--><!--teleport anchor-->`,
61+
)
6062
})
6163

6264
test('teleport rendering (vnode)', async () => {
@@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
7375
)
7476
expect(html).toBe('<!--teleport start--><!--teleport end-->')
7577
expect(ctx.teleports!['#target']).toBe(
76-
'<span>hello</span><!--teleport anchor-->',
78+
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->',
7779
)
7880
})
7981

@@ -93,7 +95,9 @@ describe('ssrRenderTeleport', () => {
9395
expect(html).toBe(
9496
'<!--teleport start--><span>hello</span><!--teleport end-->',
9597
)
96-
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
98+
expect(ctx.teleports!['#target']).toBe(
99+
`<!--teleport start anchor--><!--teleport anchor-->`,
100+
)
97101
})
98102

99103
test('multiple teleports with same target', async () => {
@@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => {
115119
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
116120
)
117121
expect(ctx.teleports!['#target']).toBe(
118-
'<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->',
122+
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->' +
123+
'<!--teleport start anchor-->world<!--teleport anchor-->',
119124
)
120125
})
121126

@@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => {
134139
)
135140
expect(html).toBe('<!--teleport start--><!--teleport end-->')
136141
expect(ctx.teleports!['#target']).toBe(
137-
`<div>content</div><!--teleport anchor-->`,
142+
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
138143
)
139144
})
140145

@@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => {
169174
await p
170175
expect(html).toBe('<!--teleport start--><!--teleport end-->')
171176
expect(ctx.teleports!['#target']).toBe(
172-
`<div>content</div><!--teleport anchor-->`,
177+
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
173178
)
174179
})
175180
})

packages/server-renderer/src/helpers/ssrRenderTeleport.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ export function ssrRenderTeleport(
2929

3030
if (disabled) {
3131
contentRenderFn(parentPush)
32-
teleportContent = `<!--teleport anchor-->`
32+
teleportContent = `<!--teleport start anchor--><!--teleport anchor-->`
3333
} else {
3434
const { getBuffer, push } = createBuffer()
35+
push(`<!--teleport start anchor-->`)
3536
contentRenderFn(push)
3637
push(`<!--teleport anchor-->`)
3738
teleportContent = getBuffer()

0 commit comments

Comments
 (0)