Multiple Root Nodes and Attribute Inheritance in Vue 3

  You block advertising 😢
Would you like to buy me a ☕️ instead?

A tweet by Manuel Matuzović reminded me that in Vue 3, we finally have fragments. But I also remembered that this is not without its problems.

<template>
  <!-- ✅ -->
  <SingleRootNodeComponent class="myClass"/>
  <!-- ❌ No attr inheritance with multiple root nodes! -->
  <MultiNodeComponent class="myClass"/>
</template>
<!-- MultiNodeComponent.vue -->
<template>
  <div><!-- ... --></div>
  <div><!-- ... --></div>
</template>

Although using class on MultiNodeComponent will log a warning to the console, I think it leads to a suboptimal developer experience when using MultiNodeComponent.

You can work around this specific limitation by manually defining one of the root nodes to inherit the attributes applied to the component instance. But this has the potential to be even more confusing and should be used with great care.

<!-- MultiNodeComponent.vue -->
<template>
  <div><!-- ... --></div>
  <!-- No warnings, `$attrs` are passed to the second `<div>`. -->
  <div v-bind="$attrs"><!-- ... --></div>
</template>

I’ve always wondered if the Vue convenience feature that automatically adds non-prop attributes to the root node’s attributes is an anti pattern. There might be a reason, why in the React world, this does not exist.

// ParentComponent.jsx
export const ParentComponent = () =>
  <div className="parent-component">
    <ChildComponent className="parent-component__child">
      ...
    </ChildComponent>
  </div>

// ChildComponent.jsx
// In React, we must explicitly define a prop if we want
// to pass a CSS class to a child component. There is no
// automatic attribute inheritance!
export const ChildComponent = ({ className, children }) =>
  <div className={className}>
    {children}
  </div>

While admittedly often very handy, it also makes it easy to do nasty things. For example, apply CSS classes that override the styles of a component from outside. Which I strongly advise against.

<template>
  <!-- ❌ -->
  <MyComponent class="justify-center"/>
</template>

At the time we apply justify-center on the MyComponent instance, it might have a display: flex style. But as soon as somebody changes the display style inside of MyComponent to block, this instance breaks, possibly without anyone noticing because the developer who changes it only looks at a different instance of it that doesn’t have justify-center applied to it.

<template>
  <!-- ✅ -->
  <MyComponent center/>
</template>

Now we only rely on the public API of MyComponent to center its contents. The concrete implementation of how to center its contents is now up to the component itself. If anybody changes MyComponent to display: block, it should be obvious that they must take the center prop into account because it’s part of its public API.

<template>
  <!-- ❌ -->
  <MyTrigger disabled/>
</template>

In this example, we apply the disabled attribute on the MyTrigger component instance. But again, we rely on implementation details of MyTrigger. Suppose MyTrigger is changed to not use a <button> as its root node, then the disabled attribute on this particular instance does nothing anymore. It never was part of the public API (props and events) of MyTrigger, so whoever changes the (supposedly) private API does not expect this to be a potential breaking change. However, effectively, automatic attribute inheritance results in the root node of a component being part of its public API, which increases its API surface quite a bit. Therefore, whenever we change the public API, we must check every component instance to make sure none breaks.

Wrapping It Up

So as we can see, automatic non-prop attribute inheritance can be dangerous. Multiple root nodes exacerbate the problem because now we may not be able to apply attributes to specific components if they render multiple root nodes instead of only one that can inherit the attributes.

Maybe this is a good argument for setting up a rule for your projects not to allow non-prop attributes on components? Doing so very often violates the principle of treating components as a black box.

I still use non-prop attributes like class and disabled on component instances. But at least with class, I see people misusing it over and over again, which is rather frustrating.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi