]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hydration): handle v-if on insertion parent
authordaiwei <daiwei521@126.com>
Tue, 29 Jul 2025 10:21:12 +0000 (18:21 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 30 Jul 2025 06:47:04 +0000 (14:47 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/insertionState.ts

index 0c0919ae405e00ae7c3261f33ec470f1e86498d1..54381d7e688d5c25d21fd4dae3b8e2738a1d5741 100644 (file)
@@ -1134,6 +1134,28 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toBe(`<div>foo</div><!---->`)
     })
 
+    test('v-if on insertion parent', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">
+            <components.Child/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+
+      data.value = true
+      await nextTick()
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+    })
+
     test('v-if/else-if/else chain - switch branches', async () => {
       const data = ref('a')
       const { container } = await testHydration(
index 56b9d9b4a217bb72c0e8cc56dbb72d4395f01bc4..bfa9e6e7af828916dd39393469add3c945c7fa77 100644 (file)
@@ -9,6 +9,37 @@ import {
 import { renderEffect } from './renderEffect'
 import { DynamicFragment } from './fragment'
 
+const ifStack = [] as DynamicFragment[]
+const insertionParents = new WeakMap<DynamicFragment, Node[]>()
+
+/**
+ * Collects insertionParents inside an if block during hydration
+ * When the if condition becomes false on the client, clears the
+ * HTML of these insertionParents to prevent duplicate rendering
+ * results when the condition becomes true again
+ *
+ * Example:
+ * const t2 = _template("<div></div>")
+ * const n2 = _createIf(() => show.value, () => {
+ *   const n5 = t2()
+ *   _setInsertionState(n5)
+ *   const n4 = _createComponent(Comp) // renders `<span></span>`
+ *   return n5
+ * })
+ *
+ * After hydration, the HTML of `n5` is `<div><span></span></div>` instead of `<div></div>`.
+ * When `show.value` becomes false, the HTML of `n5` needs to be cleared,
+ * to avoid duplicated rendering when `show.value` becomes true again.
+ */
+export function collectInsertionParents(insertionParent: ParentNode): void {
+  const currentIf = ifStack[ifStack.length - 1]
+  if (currentIf) {
+    let nodes = insertionParents.get(currentIf)
+    if (!nodes) insertionParents.set(currentIf, (nodes = []))
+    nodes.push(insertionParent)
+  }
+}
+
 export function createIf(
   condition: () => any,
   b1: BlockFn,
@@ -27,7 +58,19 @@ export function createIf(
       isHydrating || __DEV__
         ? new DynamicFragment(IF_ANCHOR_LABEL)
         : new DynamicFragment()
+    if (isHydrating) {
+      ;(frag as DynamicFragment).teardown = () => {
+        const nodes = insertionParents.get(frag as DynamicFragment)
+        if (nodes) {
+          nodes.forEach(p => ((p as Element).innerHTML = ''))
+          insertionParents.delete(frag as DynamicFragment)
+        }
+        ;(frag as DynamicFragment).teardown = undefined
+      }
+      ifStack.push(frag as DynamicFragment)
+    }
     renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
+    isHydrating && ifStack.pop()
   }
 
   if (!isHydrating && _insertionParent) {
index 9f51fac3827bd67f0b535a16a6947cf91703522d..50ce18bdb3ac4fd505eef84c5fe3280ad46741cf 100644 (file)
@@ -59,6 +59,7 @@ export class DynamicFragment extends VaporFragment {
    * indicates forwarded slot
    */
   forwarded?: boolean
+  teardown?: () => void
 
   constructor(anchorLabel?: string) {
     super([])
@@ -97,6 +98,7 @@ export class DynamicFragment extends VaporFragment {
     // teardown previous branch
     if (this.scope) {
       this.scope.stop()
+      if (parent) this.teardown && this.teardown()
       const mode = transition && transition.mode
       if (mode) {
         applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
index 5c4c41fe23c2b2ea8edb6395fab4acf9441edb3a..5c8ba4262e76c4103bdc47aa4cfe76f829577694 100644 (file)
@@ -1,3 +1,6 @@
+import { collectInsertionParents } from './apiCreateIf'
+import { isHydrating } from './dom/hydration'
+
 export let insertionParent:
   | (ParentNode & {
       // dynamic node position - hydration only
@@ -21,6 +24,10 @@ export let insertionAnchor: Node | 0 | undefined
 export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
   insertionParent = parent
   insertionAnchor = anchor
+
+  if (isHydrating) {
+    collectInsertionParents(parent)
+  }
 }
 
 export function resetInsertionState(): void {