Building “Renderless” Vue Components

There's this popular analogy of Vue that goes like this: Vue is what you get when React and Angular come together and make a baby. I've always shared this feeling. With Vue’s small learning curve, it's no wonder so many people love it. Since Vue tries to give the developer power over components and their implementation as much as it possibly can, this sentiment has led to today's topic.

The term renderless components refers to components that don’t render anything. In this article, we'll cover how Vue handles the rendering of a component.

We'll also see how we can use the render() function to build renderless components.

You may want to know a little about Vue to get the most out of this article. If you are a newbie Sarah Drasner's got your back. The official documentation is also a very good resource.

Demystifying How Vue Renders a Component

Vue has quite a few ways of defining a component's markup. There's:

  • Single file components that let us define components and their markup like we would a normal HTML file.
  • The template component property which allows us to use JavaScript's template literals to define the markup for our component.
  • The el component property tells Vue to query the DOM for a markup to use as the template.

You've probably heard the (possibly rather annoying): at the end of the day, Vue and all its components are just JavaScript. I could see why you may think that statement is wrong with the amount of HTML and CSS we write. Case and point: single file components.

With single file components, we can define a Vue component like this:

<template>
  <div class="mood">
    {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
  </div>
</template>

<script>
  export default {
    data: () => ({ todayIsSunny: true })
  }
</script>

<style>
  .mood:after {
    content: '&#x1f389;&#x1f389;';
  }
</style>

How can we say Vue is "just JavaScript" with all of that gobbledygook above? But, at the end of the day, it is. Vue does try to make it easy for our views to manage their styling and other assets, but Vue doesn't directly do that — it leaves it up to the build process, which is probably webpack.

When webpack encounters a .vue file, it'll run it through a transform process. During this process, the CSS is extracted from the component and placed in its own file, and the remaining contents of the file get transformed into JavaScript. Something like this:

export default {
  template: `
    <div class="mood">
      {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
    </div>`,
  data: () => ({ todayIsSunny: true })
}

Well... not quite what we have above. To understand what happens next, we need to talk about the template compiler.

The Template Compiler and the Render Function

This part of a Vue component’s build process is necessary for compiling and running every optimization technique Vue currently implements.

When the template compiler encounters this:

{
  template: `<div class="mood">...</div>`,
  data: () => ({ todayIsSunny: true })
}

...it extracts the template property and compiles its content into JavaScript. A render function is then added to the component object. This render function will, in turn, return the extracted template property content that was converted into JavaScript.

This is what the template above will look like as a render function:

...
render(h) {
  return h(
    'div',
    { class: 'mood' },
    this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
  )
}
...

Refer to the official documentation to learn more about the render function.

Now, when the component object gets passed to Vue, the render function of the component goes through some optimizations and becomes a VNode (virtual node). VNode is what gets passed into snabbdom (the library Vue uses internally to manage the virtual DOM). Sarah Drasner does a good job explaining the "h" in the render function above.

A VNode is how Vue renders components. By the way, the render function also allows us to use JSX in Vue!

We also don’t have to wait for Vue to add the render function for us — we can define a render function and it should take precedence over el or the template property. Read here to learn about the render function and its options.

By building your Vue components with Vue CLI or some custom build process, you don't have to import the template compiler which can bloat your build file size. Your components are also pre-optimized for a brilliant performance and really lightweight JavaScript files.

So... Renderless Vue Components

Like I mentioned, the term renderless components means components that don’t render anything. Why would we want components that don't render anything?

We can chalk it up to creating an abstraction of common component functionality as its own component and extending said component to create better and even more robust components. Also, S.O.L.I.D.

According to the Single responsibility principle of S.O.L.I.D.:

A class should only have one purpose.

We can port over that concept into Vue development by making each component have only one responsibility.

You may be like Nicky and, "pfft, yeah I know." Okay, sure! Your component may have the name "password-input" and it surely renders a password input. The problem with this approach is that when you want to reuse this component in another project, you may have to go into the component’s source to modify the style or the markup to keep in line with the new project’s style guide.

This breaks a rule of S.O.L.I.D. known as the open-closed principle which states that:

A class or a component, in this case, should be open for extension, but closed for modification.

This is saying that instead of editing the component's source, you should be able to extend it.

Since Vue understands S.O.L.I.D. principles, it lets components have props, events, slots, and scoped slots which makes communication and extension of a component a breeze. We can then build components that have all the features, without any of the styling or markup. Which is really great for re-usability and efficient code.

Build a "Toggle" Renderless Component

This will be simple. No need to set up a Vue CLI project.

The Toggle component will let you toggle between on and off states. It'll also provide helpers that let you do that. It's useful for building components like, well, on/off components such as custom checkboxes and any components that need an on/off state.

Let's quickly stub our component: Head to the JavaScript section of a CodePen pen and follow along.

// toggle.js
const toggle = {
  props: {
    on: { type: Boolean, default: false }
  },
  render() {
    return []
  },
  data() {
    return { currentState: this.on }
  },
  methods: {
    setOn() {
      this.currentState = true
    },
    setOff() {
      this.currentState = false
    },
    toggle() {
      this.currentState = !this.currentState
    }
  }
}

This is pretty minimal and not yet complete. It needs a template, and since we don't want this component to render anything, we have to make sure it works with any component that does.

Cue the slots!

Slots in Renderless Components

Slots allow us to place content between the opening and close tags of a Vue component. Like this:

<toggle>
  This entire area is a slot.
</toggle>

In Vue's single file components, we could do this to define a slot:

<template>
  <div>
    <slot/>
  </div>
</template>

Well, to do that using a render() function, we can:

// toggle.js
render() {
  return this.$slots.default
}

We can automatically place stuff within our toggle component. No markup, nothing.

Sending Data up the Tree with Scoped Slots

In toggle.js, we had some on state and some helper functions in the methods object. It’d be nice if we could give the developer access to them. We are currently using slots and they don’t let us expose anything from the child component.

What we want is scoped slots. Scoped slots work exactly like slots with the added advantage of a component having a scoped slot being able to expose data without firing events.

We can do this:

<toggle>
  <div slot-scope="{ on }">
    {{ on ? 'On' : 'Off' }}
  </div>
</toggle>

That slot-scope attribute you can see on the div is de-structuring an object exposed from the toggle component.

Going back to the render() function, we can do:

render() {
  return this.$scopedSlots.default({})
}

This time around, we are calling the default property on the $scopedSlots object as a method because scoped slots are methods that take an argument. In this case, the method's name is default since the scoped slot wasn't given a name and it's the only scoped slot that exists. The argument we pass to a scoped slot can then be exposed from the component. In our case, let’s expose the current state of on and the helpers to help manipulate that state.

render() {
  return this.$scopedSlots.default({
    on: this.currentState,
    setOn: this.setOn,
    setOff: this.setOff,
    toggle: this.toggle,
  })
}

Using the Toggle Component

We can do this all in CodePen. Here is what we’re building:

See the Pen ZRaYWm by Samuel Oloruntoba (@kayandrae07) on CodePen.

Here’s the markup in action:

<div id="app">
  <toggle>
    <div slot-scope="{ on, setOn, setOff }" class="container">
      <button @click="click(setOn)" class="button">Blue pill</button>
      <button @click="click(setOff)" class="button isRed">Red pill</button>
      <div v-if="buttonPressed" class="message">
        <span v-if="on">It's all a dream, go back to sleep.</span>
        <span v-else>I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes.</span>
      </div>
    </div>
  </toggle>
</div>
  1. First, we are de-structuring the state and helpers from the scoped slot.
  2. Then, within the scoped slot, we created two buttons, one to toggle current state on and the other off.
  3. The click method is just there to make sure a button was actually pressed before we display a result. You can check out the click method below.
new Vue({
  el: '#app',
  components: { toggle },
  data: {
    buttonPressed: false,
  },
  methods: {
    click(fn) {
      this.buttonPressed = true
      fn()
    },
  },
})

We can still pass props and fire events from the Toggle component. Using a scoped slot changes nothing.

new Vue({
  el: '#app',
  components: { toggle },
  data: {
    buttonPressed: false,
  },
  methods: {
    click(fn) {
      this.buttonPressed = true
      fn()
    },
  },
})

This is a basic example, but we can see how powerful this can get when we start building components like a date picker or an autocomplete widget. We can reuse these components across multiple projects without having to worry about those pesky stylesheets getting in the way.

One more thing we can do is expose the attributes needed for accessibility from the scoped slot and also don’t have to worry about making components that extend this component accessible.

In Summary

  • A component’s render function is incredibly powerful.
  • Build your Vue components for a faster run-time.
  • Component’s el, template or even single file components all get compiled into render functions.
  • Try to build smaller components for more reusable code.
  • Your code need not be S.O.L.I.D., but it’s a damn good methodology to code by.

Sources