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
Middlecomponent disables attribute inheritance. The test confirms that theclassattribute does not reach theChild, 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.