unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
vuejs/core
mid7/10
api-designbackwards-compatibilitytesting-strategy

fix(runtime-vapor): preserve vdom props normalization in interop

Project: vuejs/core | Language: TypeScript | Concepts: api-design, backwards-compatibility, testing-strategy | Difficulty: mid
Link: https://github.com/vuejs/core/pull/15261

The problem

When using Vue's Vapor renderer to interoperate with VDOM components, inherited and default props (like modelValue) were not being resolved correctly. This led to issues where boolean prop defaults were ignored, and internal props could incorrectly appear in $attrs, breaking expected component behavior.

Why it's non-trivial

A naive fix might simply pass the component's props definition directly, but this would skip important normalization and merging logic that Vue's VDOM runtime applies—especially for inherited props and default values. This can cause subtle bugs, such as missing default values or leaking internal props into the public attributes.

The approach

1. Regression test for inherited prop resolution

A new test ensures that inherited props (like modelValue) are resolved correctly and don't leak into $attrs:

+    test('should resolve inherited props when vapor renders a vdom component', () => {
+      const VDomChild = defineComponent({
+        extends: {
+          props: {
+            modelValue: { type: Boolean, default: false },
+          },
+        },
+        setup(props: any, { attrs }) {
+          return () =>
+            h(
+              'div',
+              `${typeof props.modelValue}:${String(props.modelValue)}:${String('model-value' in attrs)}`,
+            )
+        },
+      })
+      const App = compile(
+        `<template>
+          <components.VDomChild :model-value="false" />
+          <components.VDomChild />
+        </template>`,
+        ref(null),
+        { VDomChild },
+      )
+
+      const { html } = define(App as any).render()
+
+      expect(html()).toBe(
+        '<div>boolean:false:false</div><div>boolean:false:false</div>',
+      )
+    })

This test checks both explicit and defaulted modelValue props, and ensures model-value is not present in $attrs.

2. Use VDOM's normalized prop options in the Vapor wrapper

Instead of passing the raw component prop definition, the code now reuses the VDOM instance's normalized prop options, preserving all merging and normalization logic:

-  const wrapper = new VaporComponentInstance<Record<string, unknown>>(
-    useBridge ? (comp as any) : { props: component.props },
-    rawProps as RawProps,
-    rawSlots as RawSlots,
-    parentComponent ? parentComponent.appContext : undefined,
-    once,
-  )
-
   // overwrite how the vdom instance handles props
   vnode.vi = (instance: ComponentInternalInstance) => {
+    // Reuse VDOM's normalized options so Options API merging stays in VDOM.
+    const wrapper = new VaporComponentInstance<Record<string, unknown>>(
+      useBridge
+        ? (comp as any)
+        : {
+            props: instance.propsOptions[0],
+            __propsOptions: instance.propsOptions,
+          },
+      rawProps as RawProps,
+      rawSlots as RawSlots,
+      parentComponent ? parentComponent.appContext : undefined,
+      once,
+    )

This ensures that any prop inheritance, defaulting, and normalization performed by the VDOM runtime is preserved when bridging to Vapor.

3. Ensure props remain shallow reactive

     // ensure props are shallow reactive to align with VDOM behavior.
     instance.props = shallowReactive(wrapper.props)

This maintains reactivity semantics between the two runtimes.

From the review thread

No notable review discussion on this PR.

Takeaways

  • Always use the runtime's normalized/merged prop definitions when bridging component systems to preserve inheritance and default value logic.
  • Tests for interop layers should verify both explicit and default prop values, as well as the absence of internal props in public attributes.
  • When wrapping or bridging component instances, ensure that reactivity semantics (like shallow reactivity) are preserved to avoid subtle bugs.