]> 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 02:07:39 +0000 (10:07 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/insertionState.ts

index 728c1568d8406aefb43d6b60181792adcfdb2221..ffe2e8f098e9ff0fb6af9ca3353cbdbfc3279e31 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 3e370592b32d118e36736ea7ecee0ec77beaf263..cda16f314d981e3644b881b794ad20b2f248bd13 100644 (file)
@@ -8,6 +8,37 @@ import {
 } from './insertionState'
 import { renderEffect } from './renderEffect'
 
+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,
@@ -26,7 +57,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 b1eaaf4968af527fe47ec63a73982cfa0d2b8f3d..dc6ae4666551b8d88e8cf0970d53ea7bc7cfdb34 100644 (file)
@@ -40,6 +40,7 @@ export class DynamicFragment extends VaporFragment {
   scope: EffectScope | undefined
   current?: BlockFn
   fallback?: BlockFn
+  teardown?: () => void
 
   constructor(anchorLabel?: string) {
     super([])
@@ -64,7 +65,10 @@ export class DynamicFragment extends VaporFragment {
     // teardown previous branch
     if (this.scope) {
       this.scope.stop()
-      parent && remove(this.nodes, parent)
+      if (parent) {
+        remove(this.nodes, parent)
+        this.teardown && this.teardown()
+      }
     }
 
     if (render) {
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 {