lukes.tips

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 values.

Example

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.

Parent.vue
<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.

Child.vue
<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.

Practical Solutions

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.

Parent.vue
<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 practical 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.