Adventures in Vue's render function
Tags: vue, rendering, javascript, component
Published: 2020-01-08 21:53:28

---
title: Adventures in Vue's render function
published: true
description: Things I learned while building my first Vue component via render function
tags: Vue, rendering, JavaScript, component
---

In the last days, I built my first Vue component using a `render` function instead of the common `<template>` tag.

Now, my impostor syndrome tells me that the things I've learned while doing so are totally obvious to anyone else using Vue.

Maybe that's the case – and maybe it isn't. I hope you can take something away from this story. Or tell me where I've overseen obvious flaws in the solution :see_no_evil:

### Why did I use the render function?

What I've build is a tabs component. I had a look at different existing solutions for that. I also talked with colleagues about a nice interface for such a component. We all liked the way Vuetify handles the case. You just throw in some `<Tab>`s and the same number of `<TabItem>`s and the `<Tabs>` component magically takes care of toggling the content and active states:

```html
<AwesomeTabs>
  <MyTab>Tab #1</MyTab>
  <MyTab>Tab #2</MyTab>
  <MyTab>Tab #3</MyTab>
  <MyTabContent>Content #1</MyTabContent>
  <MyTabContent>Content #2</MyTabContent>
  <MyTabContent>Content #3</MyTabContent>
</AwesomeTabs>
```

With a structure like this, you can't simply throw everything into the `default` slot of a Vue `template`. You do not want to render the `<Tab>`s and `<TabItem>`s next to each other. Instead, this needs some logic to toggle an `active` state for the currently selected `<Tab>` and only show the currently selected `<TabItem>`.

### How the render function works

Of course, you should check the [Vue documentation on render functions](https://vuejs.org/v2/guide/render-function.html). Quick TL;DR here:

- The `render` function returns whatever you want to render, either within your `<script>` block of a `.vue` single file component (no `<template>` tag needed then) or from a pure `.js` file.
- Into `render`, you'll pass (and use) the `createElement` function (often shortened to `h`) to create each `VNode` (virtual nodes) that Vue then handles.
- Everything you normally do within the `template` tag is basically sugar coating for the actually used `render` function.

Simple example:

```js
render(createElement) {
  return createElement(
    'h1', // the element you want to render, could also be a Vue component
    {
      // this is the options object which is… hehe… optional, e.g.:
      class: 'ohai-css',
      attrs: {
        id: 'mightyID'
      },
      on: {
        click: this.handleClick
      },
    }, 
    'Hello world!' // the content – a text string or an array of other VNodes
  ) 
}

```

So let's have a look at how I fought my way towards a working tabs component. We'll take my AHA moments as guideposts.

### `this.$slots.default` is always filled!

Something I had never thought about (but makes sense): Even if you have a "closed" component, you can throw any content into it and it is available under `this.$slots.default`. Check the `HelloWorld.vue` in [this code sandbox](https://codesandbox.io/s/competent-fast-9wmmd). The content is not rendered in the component, but it is there.

With that, you can easily filter the components as needed – in my case, it was enough to check for the name of the component:

```js
const tabItems = this.$slots.default
  .filter(item => item.componentOptions.tag === "MyTab")
```

### Don't manipulate, duplicate!

So I had access this list of components within my `Tabs`. My first though was: Nice, I'll just™ split this up into the tab navigation and the tab content, slap an `index` plus an `onClick` handler onto the tab navigation items and off we go.

That totally did *NOT* work :smile:

What I had to do instead was to take the list of navigation items, create a new element for each one and add the necessary props to *that* component instance:

```js
const tabItems = this.$slots.default
  .filter(item => item.componentOptions.tag === "MyTab") // filter for navigation items
  .map((item, index) =>
    h( // render a new component…
      MyTab, // of the same type
      {
        props: { // pass props
          index,
          isActive: this.selectedIndex === index // selectedIndex is declared within data
        },
        on: {
          onClick: this.switchTab // which takes the emitted index and sets selectedIndex to that
        }
      },
      item.componentOptions.children // use content from the original component
    )
  );
```

My uneducated, clueless guess here is: The components are already rendered. Vue doesn't let you touch them or alter their props within the `render` function because that will break… the internet? :smile:

You _have_ to render completely new component instances instead. This most certainly makes sense – if you know why, please explain in the comments :wink:


### Reading documentation carefully actually helps!

Having achieved all of this, I was very happy and wanted to render the tab navigation and the current content like this:

```js
return h(
  "div", // render a surrounding container
  [ // with children
    h("ul", { class: "tabListNav" }, tabItems), // tab navigation
    h('main', tabContent) // current tab content
  ])
```

Aaaand… no content was rendered `¯\_(ツ)_/¯`

So I re-read the [`createElement` arguments part](https://vuejs.org/v2/guide/render-function.html#createElement-Arguments) of the documentation again. And of course, it was a very simple fix: You can either pass a string as the child of an element. Or an array of items. Even if you just want to render one item, you have to put it into an array. Spot the difference:

```js
return h(
  "div", // render a surrounding container
  [ // with children
    h("ul", { class: "tabListNav" }, tabItems), // tab navigation
    h('main', [tabContent]) // current tab content passed in an array
  ])
```

:tada: With all of this, we have a nice tab component that fulfills everything I needed:

- Render a tab navigation
- Render the correct content
- Easy to use because state handling etc. is tugged away in `<AwesomeTabs>`

Of course, you could add a great deal of functionality, but I don't need to :smile:

Here's a code sandbox with everything in it:

{% codesandbox boring-chaum-3zn7i %}