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>
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
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.