unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
vuejs/core
senior7/10
consistency-modelstesting-strategyapi-design

fix(runtime-vapor): preserve nested vdom slot content (fix #15303)

Project: vuejs/core | Language: TypeScript | Concepts: consistency-models, testing-strategy, api-design | Difficulty: senior
Link: https://github.com/vuejs/core/pull/15304

The problem

When using Vue's Vapor mode with VDOM (Virtual DOM) interop, slot content nested inside a <Transition> component could be incorrectly handled. Specifically, VDOM slot outlets directly under a Vapor <Transition> root were sometimes treated as direct transition children too early, leading to issues like lost slot content or incorrect transition behavior, especially during hydration or when using v-once.

Why it's non-trivial

A naive fix might simply adjust how slot content is rendered, but this would miss the subtle timing and ownership issues between Vapor and VDOM branches. The problem only appears in specific interop scenarios, such as when a compiled Vapor child is consumed by a compiled VDOM parent, or when hydration and transition hooks interact. The solution needs to preserve transition branch selection and ensure slot content is not dropped or mismanaged, even with optimizations like v-once.

The approach

1. Pass new context about slot root status

A new isDirectSlotRoot boolean is introduced and threaded through the slot transition preparation logic to distinguish direct slot roots from other cases:

function prepareInteropSlotTransition(
  frag: RenderContextFragment,
  vnode: VNode,
+  isDirectSlotRoot: boolean,
  previous: VNode | undefined,
  resumeAfterLeave: () => void,
  delayedLeaveSource?: TransitionHooks,

This flag is set at the slot rendering call site:

const isDirectSlotRoot = !!(slotRoot || sharedFallback || inheritFallback)

This helps the code later decide if special handling is needed for direct slot content under a transition.

2. Refine transition hook propagation logic

The main slot transition logic is updated to use the new flag, ensuring that only the correct slot children are treated as direct transition children before hooks are propagated:

if (
  !transition &&
-  !(instance && isVaporTransition(instance.type as VaporComponent))
+  !(
+    isDirectSlotRoot &&
+    instance &&
+    isVaporTransition(instance.type as VaporComponent)
+  )
) {
  return
}

This change ensures that only direct slot roots under a Vapor <Transition> are given special transition handling, avoiding premature or incorrect transition branch selection for nested or indirect slot content.

3. Update all transition preparation call sites

Every call to prepareInteropSlotTransition is updated to pass the new isDirectSlotRoot argument, ensuring consistent behavior throughout the slot rendering logic:

const nextVNode =
-  prepareInteropSlotTransition(frag, content, undefined, NOOP) ||
-  content
+  prepareInteropSlotTransition(
+    frag,
+    content,
+    isDirectSlotRoot,
+    undefined,
+    NOOP,
+  ) || content

This pattern is repeated for all relevant slot rendering paths.

4. Add regression tests

Two new tests are added to cover these edge cases:

  • Multi-element VDOM slot content nested under a transition root is preserved and not warned against.
  • Hydration of direct v-once VDOM slot content inside a Vapor <Transition> works correctly, with the slot content and DOM order preserved.

Example test snippet:

test('preserves multi-element vdom slot content nested under transition root', () => {
  // ...setup...
  expect(
    '<transition> can only be used on a single element or component',
  ).not.toHaveBeenWarned()
  expect(
    Array.from(host.querySelectorAll('.with-transition > button')).map(
      el => el.textContent,
    ),
  ).toEqual(['First', 'Last'])
})

From the review thread

No notable review discussion on this PR.

Takeaways

  • When integrating different rendering strategies (like Vapor and VDOM), always explicitly track and propagate ownership and context information to avoid subtle bugs, especially around lifecycle hooks and transitions.
  • Regression tests for edge cases—such as transitions, hydration, and optimizations like v-once—are essential when fixing interop bugs, as these scenarios often expose hidden consistency issues.
  • Avoid over-broad logic in core rendering paths; use precise flags or metadata to distinguish special cases, preventing accidental breakage in unrelated scenarios.