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

fix(runtime-vapor): respect inheritAttrs ownership for component roots

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

The problem

Vapor components in Vue were not correctly respecting the inheritAttrs: false option on intermediate components. This led to attributes (like class) being unintentionally forwarded to child components, even when an intermediate component explicitly opted out of attribute inheritance. This broke expected encapsulation and could cause bugs in complex component hierarchies.

Why it's non-trivial

A naive fix might only check the inheritAttrs setting on the immediate child or on the component itself, missing cases where intermediate components in the tree have disabled attribute inheritance. Correct behavior requires that attribute fallthrough stops as soon as any component in the chain sets inheritAttrs: false, not just at the leaf or at the root.

The approach

1. Fixing the attribute inheritance check in the runtime

Before:

-      component.inheritAttrs !== false &&

The old code only checked component.inheritAttrs, which could miss the current instance's configuration.

After:

+      currentInstance.type.inheritAttrs !== false &&

Now, the check is against currentInstance.type.inheritAttrs, ensuring that the attribute inheritance setting of the current component instance is respected. This means if any component in the hierarchy sets inheritAttrs: false, attributes will not fall through past that point.

Annotated:

  • This change ensures that attribute fallthrough is blocked as soon as a component in the hierarchy disables it, not just at the root or at the component definition level.

2. Adding regression tests for both forwarding and blocking scenarios

Test: Attributes are forwarded if only the child disables inheritance

it('should pass fallthrough attrs to declared props with inheritAttrs: false', () => {
  const Child = compile(
    `<script setup vapor>
      const { class: className } = defineProps({ class: String })
      defineOptions({ inheritAttrs: false })
    </script>
    <template>
      <div :class="className">class prop = {{ className ?? 'DROPPED' }}</div>
    </template>`,
    ref(null),
  )
  const Middle = compile(
    `<template><components.Child /></template>`,
    ref(null),
    { Child },
  )
  const App = compile(
    `<template><components.Middle class="forwarded-marker" /></template>`,
    ref(null),
    { Middle },
  )

  const { host } = define(App).render()
  expect(host.innerHTML).toBe(
    '<div class="forwarded-marker">class prop = forwarded-marker</div>',
  )
})

Annotated:

  • This test ensures that when only the child disables inheritAttrs, the parent can still forward attributes (like class) as props.

Test: Attributes are blocked by an intermediate component

it('should not pass attrs through a component with inheritAttrs: false', () => {
  const Child = compile(
    `<script setup vapor>
      const { class: className } = defineProps({ class: String })
    </script>
    <template>
      <div :class="className">class prop = {{ className ?? 'DROPPED' }}</div>
    </template>`,
    ref(null),
  )
  const Middle = compile(
    `<script setup vapor>
      const Child = _components.Child
      defineOptions({ inheritAttrs: false })
    </script>
    <template><Child /></template>`,
    ref(null),
    { Child },
  )
  const App = compile(
    `<template><components.Middle class="forwarded-marker" /></template>`,
    ref(null),
    { Middle },
  )

  const { host } = define(App).render()
  expect(host.innerHTML).toBe('<div>class prop = DROPPED</div>')
})

Annotated:

  • Here, the intermediate Middle component disables attribute inheritance. The test confirms that the class attribute does not reach the Child, and the prop is dropped as expected.

From the review thread

No notable review discussion on this PR.

Takeaways

  • Always check configuration options (like inheritAttrs) at the correct instance level, not just on the component definition, to ensure expected behavior in nested hierarchies.
  • Regression tests should cover both the positive (attributes forwarded) and negative (attributes blocked) cases to prevent future breakage.
  • Attribute fallthrough logic must account for the entire component chain, not just the immediate parent or child, to maintain encapsulation and predictable behavior.