Skip to content

Commit 1cc4870

Browse files
committed
fix(hydration): defer forwarded slot fallback hydration cleanup
1 parent 55cfb3d commit 1cc4870

2 files changed

Lines changed: 88 additions & 6 deletions

File tree

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9201,6 +9201,60 @@ describe('VDOM interop', () => {
92019201
)
92029202
})
92039203

9204+
test('hydrate forwarded slot fallback with nested component before parent close marker', async () => {
9205+
const data = ref('foo')
9206+
const { container } = await testWithVaporApp(
9207+
`<script setup>
9208+
const components = _components
9209+
</script>
9210+
<template>
9211+
<components.Child>
9212+
<template #foo><slot name="foo" /></template>
9213+
</components.Child>
9214+
</template>`,
9215+
{
9216+
Child: {
9217+
code: `<script setup>
9218+
const components = _components
9219+
const data = _data
9220+
</script>
9221+
<template>
9222+
<div>
9223+
<slot name="foo">
9224+
<components.GrandChild
9225+
v-if="data"
9226+
:text="data"
9227+
/>
9228+
</slot>
9229+
</div>
9230+
</template>`,
9231+
vapor: true,
9232+
},
9233+
GrandChild: {
9234+
code: `<script setup>
9235+
defineProps(['text'])
9236+
</script>
9237+
<template>
9238+
<template v-if="text">
9239+
<span v-if="text">{{ text }}</span>
9240+
</template>
9241+
</template>`,
9242+
vapor: true,
9243+
},
9244+
},
9245+
data,
9246+
)
9247+
9248+
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
9249+
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
9250+
"<div>
9251+
<!--[-->
9252+
<!--[--><span>foo</span><!--if--><!--if--><!--if--><!--]-->
9253+
<!--slot--><!--]-->
9254+
</div>"
9255+
`)
9256+
})
9257+
92049258
test('hydrate VDOM component returning Fragment', async () => {
92059259
const data = ref('foo')
92069260
const { container } = await testWithVaporApp(

packages/runtime-vapor/src/fragment.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,11 @@ export class DynamicFragment extends VaporFragment {
499499
let parentNode: Node | null
500500
let nextNode: Node | null
501501
if (forwardedSlot) {
502-
parentNode = slotAnchor!.parentNode
503-
nextNode = slotAnchor!.nextSibling
502+
// Keep the forwarded slot close marker structural for parent cleanup,
503+
// even though this fragment uses a runtime anchor after it.
504+
const anchor = markHydrationAnchor(slotAnchor!)
505+
parentNode = anchor.parentNode
506+
nextNode = anchor.nextSibling
504507
} else {
505508
const node = findBlockNode(this.nodes)
506509
parentNode = node.parentNode
@@ -511,11 +514,13 @@ export class DynamicFragment extends VaporFragment {
511514
// Otherwise detached anchors could be observed too early by traversal
512515
// logic such as `findLastChild()`.
513516
queuePostFlushCb(() => {
517+
const anchor =
518+
nextNode && nextNode.parentNode === parentNode ? nextNode : null
514519
parentNode!.insertBefore(
515520
(this.anchor = markHydrationAnchor(
516521
__DEV__ ? createComment(this.anchorLabel!) : createTextNode(),
517522
)),
518-
nextNode,
523+
anchor,
519524
)
520525
})
521526
} finally {
@@ -566,6 +571,7 @@ export let currentEmptyFragment: DynamicFragment | null | undefined
566571

567572
export class SlotFragment extends DynamicFragment {
568573
forwarded = false
574+
deferredHydrationBoundary?: () => void
569575

570576
constructor() {
571577
super(isHydrating || __DEV__ ? 'slot' : undefined, false, false)
@@ -579,6 +585,7 @@ export class SlotFragment extends DynamicFragment {
579585
let prevEndAnchor: Node | null = null
580586
let pushedEndAnchor = false
581587
let exitHydrationBoundary: (() => void) | undefined
588+
let deferHydrationBoundary = false
582589
if (isHydrating) {
583590
locateHydrationNode()
584591
if (isComment(currentHydrationNode!, '[')) {
@@ -624,12 +631,24 @@ export class SlotFragment extends DynamicFragment {
624631
// once against the final block.
625632
if (isHydrating) {
626633
this.hydrate(render == null, true)
634+
// Empty slots rendered while resolving an outer slot fallback can be
635+
// filled by that fallback immediately after render() returns.
636+
deferHydrationBoundary =
637+
!!exitHydrationBoundary &&
638+
currentEmptyFragment !== undefined &&
639+
!isValidBlock(this.nodes)
627640
}
628641
} finally {
629-
if (isHydrating && pushedEndAnchor) {
630-
setCurrentSlotEndAnchor(prevEndAnchor)
642+
if (isHydrating) {
643+
if (pushedEndAnchor) {
644+
setCurrentSlotEndAnchor(prevEndAnchor)
645+
}
646+
if (deferHydrationBoundary) {
647+
this.deferredHydrationBoundary = exitHydrationBoundary
648+
} else {
649+
exitHydrationBoundary && exitHydrationBoundary()
650+
}
631651
}
632-
exitHydrationBoundary && exitHydrationBoundary()
633652
}
634653
}
635654
}
@@ -657,6 +676,15 @@ export function renderSlotFallback(
657676
frag.nodes[0] = [fallback() || []] as Block[]
658677
} else if (frag instanceof DynamicFragment) {
659678
frag.update(fallback)
679+
if (isHydrating && frag instanceof SlotFragment) {
680+
const deferredHydrationBoundary = frag.deferredHydrationBoundary
681+
if (deferredHydrationBoundary) {
682+
frag.deferredHydrationBoundary = undefined
683+
// The fallback has now had a chance to hydrate the SSR nodes that
684+
// originally belonged to the empty forwarded slot.
685+
deferredHydrationBoundary()
686+
}
687+
}
660688
}
661689
return block
662690
}

0 commit comments

Comments
 (0)