DEV Community

loading...

Adventures in Vue's render function

herrbertling profile image Markus Siering ・4 min read

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 🙈

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:

<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. 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:

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. 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:

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 😄

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:

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? 😄

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

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:

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 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:

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
  ])

🎉 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 😄

Here's a code sandbox with everything in it:

Discussion (2)

pic
Editor guide
Collapse
inozex profile image
Tiago Marques

Thank you for the enlightening post :)

Collapse
herrbertling profile image
Markus Siering Author

Tiago, thank you for reading :) Hope it was helpful!