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:

<template>
  <div> <!-- The root -->
    <span></span> <!-- now we can have siblings -->
    <span></span>
  </div>
</template>

Try to build a component with a template like this:

<template>
  <span></span> <!-- two siblings at the top level of the hierarchy! -->
  <span></span>
</template>

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.

/* paired-cell.js */
import FirstCell from '~/components/paired-cell/first-cell';
import SecondCell from '~/components/paired-cell/second-cell';

export default {
  functional: true,
  props: ['person', 'place', 'thing'],
  render(createElement, context) {
    const first = createElement(FirstCell, { props: context.props });
    const second = createElement(SecondCell, { props: context.props });

    return [first, second];
  },
};

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.

  1. Functional components are stateless (They contain no data of their own, and thus their outputs are solely defined by props passed in.
  2. Functional components are instanceless, meaning there is no this context, instead props and related values are passed in via a context 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.