Creating Multi-root Vue.js Components
Affiliate disclosure: Some of the links on this page are affiliate links, which means I may receive a commission if you decide to buy a product or service I have recommended. But if you’d prefer I didn’t receive a commission, that’s cool too. Just Google the vendor’s site instead of using my link. 🙂
A common constraint in component-based frameworks like Vue.js is that each component has to have a single root element. This means that everything in a particular component has to descend from a single element, like this:
Try to build a component with a template like this:
and you will get the dreaded error: Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
In the vast majority of situations this constraint causes no problems. Have 2 elements that have to go together? Simple add another layer in the DOM hierarchy and wrap them in a div. No problem.
However, there are certain situations in which you cannot simply add an additional layer of hierarchy, situations where the structure of the DOM is super important. For example - I recently had a project where I had two <td>
elements that always had to go right next to each other. Include one and you had to include the other. Logically, they were a single component, but I couldn't just wrap them in a wrapper because <td>
elements need to be direct descendants of a <tr>
to work properly.
The Solution: Functional Components
The solution to this problem lies in an implementation detail of Vue.js. The key reason why Vue cannot currently support multi-root components lies in the template rendering mechanism - Templates for a component are parsed into an abstract syntax tree (AST), and an AST needs a root!
If you sidestep template rendering, you can sidestep the single-root limitation.
Its less commonly used, but it is entirely possible to implement a Vue.js componenent without a template at all, simply by defining a render
function. These components, known as functional components, can be used for a myriad of purposes, including rendering multiple roots in a single component.
The Code
For simplicity, I wrote each of my paired <td>
elements as its own single-file component, and then simply wrapped them in a functional component that passed along props to both of them.
FirstCell
and SecondCell
are standard Vue single file components, each with a <td>
element as the root. But PairedCell is different - it is a pure JavaScript file that exports a functional component.
There are two key differences between functional components and traditional components.
- Functional components are stateless (They contain no
data
of their own, and thus their outputs are solely defined by props passed in. - Functional components are instanceless, meaning there is no
this
context, instead props and related values are passed in via acontext
object.
Looking at what the code is doing then, it states that the component is functional, declares a set of accepted props (a person, place, and a thing), and defines a render
function that takes two arguments: createElement
and context
.
Those two arguments will be provided by Vue. createElement
is a function that sets up an element in Vue's virtual DOM. You can directly pass it element properties, but in this case I'm simply using it to render the subcomponents.
The second argument contains the context for the component; in this example the only thing we care about is the props
which we're passing along, but it also contains things like children, slots, parent, and more - all the things you might need to implement a component.
So to break down what we're doing - we implement a component that accepts a set of props, renders out two child components as siblings, and returns them as an array. Woot! A multi-root component!
Learning Vue?
If you're currently working on learning Vue, you might be interested in learning about my learning process. If you're looking for a course, I can vouch for this one, as it is the one I took to kickstart my learning.