Vue Fallthrough Attributes behaviour changes from Vue 2 to Vue 3
Recently, my team tackled migrating a large codebase from Vue 2 to Vue 3. While the Vue.js team has done an outstanding job providing comprehensive documentation that covers the recommended migration steps, and details the breaking changes, we did encounter an interesting undocumented change related to the way fallthrough attributes are handled in Vue 3.
What are Fallthrough Attributes?
Fallthrough attributes are any attributes or event listeners that are passed to a component but not explicitly defined as props
or emits
on the receiving component. Provided the receiving component renders a single root element, and the inheritAttrs
option is not set to false
, these attributes will fall through and be applied to the components root element.
There are a few documented changes relating to fallthrough attributes in Vue 3, primarily, the removal of $listeners, and $attrs now including class
and style
.
For more information on fallthrough attributes, see the Vue documentation.
Vue 3 Fallthrough Attributes overwrite explicitly set attributes
The undocumented change we encountered was that fallthrough attributes now overwrite attributes explicitly set on the root element, with the exception of class
and style
attributes as they will be merged.
Despite my attempts to find answers online, I was only able to find a single GitHub issue related to this behaviour change.
Behaviour in Vue 2 vs Vue 3
In Vue 2, fallthrough attributes could be overwritten by binding an attribute of the same name. However, in Vue 3, fallthrough attributes will consistently be applied, overwriting any explicitly set attributes.
Consider the following components, a Parent
component with no defined props. Parent
renders a Child
component as the root element and binds msg
onto the Child
component.
<script setup>
import Child from './Child.vue'
</script>
<template>
<Child msg="Set from Parent" />
</template>
Child
accepts a msg
prop and renders the value in the template.
<script setup>
defineProps({
msg: String,
})
</script>
<template>
<span>{{ msg }}</span>
</template>
In the example above, notice our Parent
does not have a msg
prop defined. Therefore, if we set msg
on our Parent
it would be considered a fallthrough attribute and automatically applied to the root element, which, in this instance, is the Child
component.
<Parent msg="Passed to Parent" />
In Vue 3, the output from this example would be “Passed to Parent” as the fallthrough msg
attribute will overwrite the explicit msg
attribute set in the Parent
component. Conversely, Vue 2 would output “Set from Parent” as the explicit msg
attribute set in the Parent
component overwrites the fallthrough msg
attribute.
How we emulated Vue 2 behaviour in Vue 3
The simplest solution is to set the inheritAttrs: false
option on our Parent
component, stopping the default fallthrough attribute behaviour. However, this isn’t always ideal, particularly if you want other fallthrough attributes to be set on the Child
. Fortunately, there’s an easy workaround.
<script setup>
import Child from './Child.vue'
defineOptions({
inheritAttrs: false,
})
</script>
<template>
<Child
v-bind="$attrs"
msg="Set from Parent"
/>
</template>
In the revised code, we’ve disabled the inheritAttrs
option but explicitly bound $attrs
(which contains all fallthrough attributes) back to our Child
component.
By setting msg="Set from Parent"
after v-bind="$attrs"
, our explicit msg
overwrites the fallthrough msg
attribute. This effectively emulates the Vue 2 behaviour, offering a simple and efficient solution to the issue.
Ideally our components wouldn’t be relying on fallthrough attributes in this way, and we do plan to refactor things so this won’t become an issue, but due to the size of the migration project we needed a quick and easy solution to this problem to wrap up the migration and get back to shipping features.