Skip to content

Commit 0ca32f4

Browse files
committed
fix(hydration): skip dynamic children in __child
1 parent f2a5abe commit 0ca32f4

File tree

4 files changed

+53
-12
lines changed

4 files changed

+53
-12
lines changed

packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
157157
const _component_Comp = _resolveComponent("Comp")
158158
const n0 = t0()
159159
const n3 = t1()
160-
const n2 = _child(n3)
160+
const n2 = _child(n3, 1)
161161
_setInsertionState(n3, 0)
162162
const n1 = _createComponentWithFallback(_component_Comp)
163163
_renderEffect(() => {

packages/compiler-vapor/src/generators/template.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ export function genChildren(
8282
pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
8383
}
8484
} else {
85+
// offset is used to determine the child during hydration.
86+
// if offset is not 0, we need to specify the offset to skip the dynamic
87+
// children and get the correct child.
88+
let childOffset = offset === 0 ? undefined : `${Math.abs(offset)}`
8589
if (elementIndex === 0) {
86-
pushBlock(...genCall(helper('child'), from))
90+
pushBlock(...genCall(helper('child'), from, childOffset))
8791
} else {
8892
// check if there's a node that we can reuse from
89-
let init = genCall(helper('child'), from)
93+
let init = genCall(helper('child'), from, childOffset)
9094
if (elementIndex === 1) {
9195
init = genCall(helper('next'), init)
9296
} else if (elementIndex > 1) {

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,43 @@ describe('Vapor Mode hydration', () => {
21382138
)
21392139
})
21402140

2141+
test('mixed consecutive slot and element', async () => {
2142+
const data = reactive({
2143+
text: 'foo',
2144+
msg: 'hi',
2145+
})
2146+
const { container } = await testHydration(
2147+
`<template>
2148+
<components.Child>
2149+
<template #foo><span>{{data.text}}</span></template>
2150+
<template #bar><span>bar</span></template>
2151+
</components.Child>
2152+
</template>`,
2153+
{
2154+
Child: `<template><div><slot name="foo"/><slot name="bar"/><div>{{data.msg}}</div></div></template>`,
2155+
},
2156+
data,
2157+
)
2158+
2159+
expect(container.innerHTML).toBe(
2160+
`<div>` +
2161+
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
2162+
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
2163+
`<div>hi</div>` +
2164+
`</div>`,
2165+
)
2166+
2167+
data.msg = 'bar'
2168+
await nextTick()
2169+
expect(container.innerHTML).toBe(
2170+
`<div>` +
2171+
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
2172+
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
2173+
`<div>bar</div>` +
2174+
`</div>`,
2175+
)
2176+
})
2177+
21412178
test('mixed slot and element', async () => {
21422179
const data = reactive({
21432180
text: 'foo',

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,26 @@ export function _child(node: ParentNode): Node {
3636
*
3737
* Client Compiled Code (Simplified):
3838
* const n2 = t0() // n2 = `<div> </div>`
39-
* const n1 = _child(n2) // n1 = text node
39+
* const n1 = _child(n2, 1) // n1 = text node
4040
* // ... slot creation ...
4141
* _renderEffect(() => _setText(n1, _ctx.msg))
4242
*
4343
* SSR Output: `<div><!--[-->slot content<!--]-->Actual Text Node</div>`
4444
*
4545
* Hydration Mismatch:
4646
* - During hydration, `n2` refers to the SSR `<div>`.
47-
* - `_child(n2)` would return `<!--[-->`.
47+
* - `_child(n2, 1)` would return `<!--[-->`.
4848
* - The client code expects `n1` to be the text node, but gets the comment.
4949
* The subsequent `_setText(n1, ...)` would fail or target the wrong node.
5050
*
5151
* Solution (`__child`):
52-
* - `__child(n2)` is used during hydration. It skips the SSR fragment anchors
53-
* (`<!--[-->...<!--]-->`) and any other non-content nodes to find the
54-
* "Actual Text Node", correctly matching the client's expectation for `n1`.
52+
* - `__child(n2, offset)` is used during hydration. It skips the dynamic children
53+
* to find the "Actual Text Node", correctly matching the client's expectation
54+
* for `n1`.
5555
*/
5656
/*! #__NO_SIDE_EFFECTS__ */
57-
export function __child(node: ParentNode): Node {
58-
let n = node.firstChild!
57+
export function __child(node: ParentNode, offset?: number): Node {
58+
let n = offset ? __nthChild(node, offset) : node.firstChild!
5959

6060
if (isComment(n, '[')) {
6161
n = locateEndAnchor(n)!.nextSibling!
@@ -157,8 +157,8 @@ type DelegatedFunction<T extends (...args: any[]) => any> = T & {
157157
}
158158

159159
/*! #__NO_SIDE_EFFECTS__ */
160-
export const child: DelegatedFunction<typeof _child> = node => {
161-
return child.impl(node)
160+
export const child: DelegatedFunction<typeof __child> = (node, offset) => {
161+
return child.impl(node, offset)
162162
}
163163
child.impl = _child
164164

0 commit comments

Comments
 (0)