Render Functions

Vue recommends using templates to build applications in the vast majority of cases. However, there are situations where we need the full programmatic power of JavaScript. That’s where we can use the render function.

Let’s dive into an example where a render() function would be practical. Say we want to generate anchored headings:

  1. <h1>
  2. <a name="hello-world" href="#hello-world">
  3. Hello world!
  4. </a>
  5. </h1>

Anchored headings are used very frequently, we should create a component:

  1. <anchored-heading :level="1">Hello world!</anchored-heading>

The component must generate a heading based on the level prop, and we quickly arrive at this:

  1. const app = Vue.createApp({})
  2. app.component('anchored-heading', {
  3. template: `
  4. <h1 v-if="level === 1">
  5. <slot></slot>
  6. </h1>
  7. <h2 v-else-if="level === 2">
  8. <slot></slot>
  9. </h2>
  10. <h3 v-else-if="level === 3">
  11. <slot></slot>
  12. </h3>
  13. <h4 v-else-if="level === 4">
  14. <slot></slot>
  15. </h4>
  16. <h5 v-else-if="level === 5">
  17. <slot></slot>
  18. </h5>
  19. <h6 v-else-if="level === 6">
  20. <slot></slot>
  21. </h6>
  22. `,
  23. props: {
  24. level: {
  25. type: Number,
  26. required: true
  27. }
  28. }
  29. })

This template doesn’t feel great. It’s not only verbose, but we’re duplicating <slot></slot> for every heading level. And when we add the anchor element, we have to again duplicate it in every v-if/v-else-if branch.

While templates work great for most components, it’s clear that this isn’t one of them. So let’s try rewriting it with a render() function:

  1. const app = Vue.createApp({})
  2. app.component('anchored-heading', {
  3. render() {
  4. const { h } = Vue
  5. return h(
  6. 'h' + this.level, // tag name
  7. {}, // props/attributes
  8. this.$slots.default() // array of children
  9. )
  10. },
  11. props: {
  12. level: {
  13. type: Number,
  14. required: true
  15. }
  16. }
  17. })

The render() function implementation is much simpler, but also requires greater familiarity with Vue instance properties. In this case, you have to know that when you pass children without a v-slot directive into a component, like the Hello world! inside of anchored-heading, those children are stored on the component instance at $slots.default(). If you haven’t already, it’s recommended to read through the instance properties API before diving into render functions.

The DOM tree

Before we dive into render functions, it’s important to know a little about how browsers work. Take this HTML for example:

  1. <div>
  2. <h1>My title</h1>
  3. Some text content
  4. </div>

When a browser reads this code, it builds a tree of “DOM nodes”Render Functions - 图1 to help it keep track of everything.

The tree of DOM nodes for the HTML above looks like this:

DOM Tree Visualization

Every element is a node. Every piece of text is a node. Even comments are nodes! Each node can have children (i.e. each node can contain other nodes).

Updating all these nodes efficiently can be difficult, but thankfully, we never have to do it manually. Instead, we tell Vue what HTML we want on the page, in a template:

  1. <h1>{{ blogTitle }}</h1>

Or in a render function:

  1. render() {
  2. return Vue.h('h1', {}, this.blogTitle)
  3. }

And in both cases, Vue automatically keeps the page updated, even when blogTitle changes.

The Virtual DOM tree

Vue keeps the page updated by building a virtual DOM to keep track of the changes it needs to make to the real DOM. Taking a closer look at this line:

  1. return Vue.h('h1', {}, this.blogTitle)

What is the h() function returning? It’s not exactly a real DOM element. It returns a plain object which contains information describing to Vue what kind of node it should render on the page, including descriptions of any child nodes. We call this node description a “virtual node”, usually abbreviated to VNode. “Virtual DOM” is what we call the entire tree of VNodes, built by a tree of Vue components.

h() Arguments

The h() function is a utility to create VNodes. It could perhaps more accurately be named createVNode(), but it’s called h() due to frequent use and for brevity. It accepts three arguments:

  1. // @returns {VNode}
  2. h(
  3. // {String | Object | Function | null} tag
  4. // An HTML tag name, a component, an async component or null.
  5. // Using null would render a comment.
  6. //
  7. // Required.
  8. 'div',
  9. // {Object} props
  10. // An object corresponding to the attributes, props and events
  11. // we would use in a template.
  12. //
  13. // Optional.
  14. {},
  15. // {String | Array | Object} children
  16. // Children VNodes, built using `h()`,
  17. // or using strings to get 'text VNodes' or
  18. // an object with slots.
  19. //
  20. // Optional.
  21. [
  22. 'Some text comes first.',
  23. h('h1', 'A headline'),
  24. h(MyComponent, {
  25. someProp: 'foobar'
  26. })
  27. ]
  28. )

Complete Example

With this knowledge, we can now finish the component we started:

  1. const app = Vue.createApp({})
  2. /** Recursively get text from children nodes */
  3. function getChildrenTextContent(children) {
  4. return children
  5. .map(node => {
  6. return typeof node.children === 'string'
  7. ? node.children
  8. : Array.isArray(node.children)
  9. ? getChildrenTextContent(node.children)
  10. : ''
  11. })
  12. .join('')
  13. }
  14. app.component('anchored-heading', {
  15. render() {
  16. // create kebab-case id from the text contents of the children
  17. const headingId = getChildrenTextContent(this.$slots.default())
  18. .toLowerCase()
  19. .replace(/\W+/g, '-') // replace non-word characters with dash
  20. .replace(/(^-|-$)/g, '') // remove leading and trailing dashes
  21. return Vue.h('h' + this.level, [
  22. Vue.h(
  23. 'a',
  24. {
  25. name: headingId,
  26. href: '#' + headingId
  27. },
  28. this.$slots.default()
  29. )
  30. ])
  31. },
  32. props: {
  33. level: {
  34. type: Number,
  35. required: true
  36. }
  37. }
  38. })

Constraints

VNodes Must Be Unique

All VNodes in the component tree must be unique. That means the following render function is invalid:

  1. render() {
  2. const myParagraphVNode = Vue.h('p', 'hi')
  3. return Vue.h('div', [
  4. // Yikes - duplicate VNodes!
  5. myParagraphVNode, myParagraphVNode
  6. ])
  7. }

If you really want to duplicate the same element/component many times, you can do so with a factory function. For example, the following render function is a perfectly valid way of rendering 20 identical paragraphs:

  1. render() {
  2. return Vue.h('div',
  3. Array.apply(null, { length: 20 }).map(() => {
  4. return Vue.h('p', 'hi')
  5. })
  6. )
  7. }

Replacing Template Features with Plain JavaScript

v-if and v-for

Wherever something can be easily accomplished in plain JavaScript, Vue render functions do not provide a proprietary alternative. For example, in a template using v-if and v-for:

  1. <ul v-if="items.length">
  2. <li v-for="item in items">{{ item.name }}</li>
  3. </ul>
  4. <p v-else>No items found.</p>

This could be rewritten with JavaScript’s if/else and map() in a render function:

  1. props: ['items'],
  2. render() {
  3. if (this.items.length) {
  4. return Vue.h('ul', this.items.map((item) => {
  5. return Vue.h('li', item.name)
  6. }))
  7. } else {
  8. return Vue.h('p', 'No items found.')
  9. }
  10. }

v-model

The v-model directive is expanded to modelValue and onUpdate:modelValue props during template compilation—we will have to provide these props ourselves:

  1. props: ['modelValue'],
  2. render() {
  3. return Vue.h('input', {
  4. modelValue: this.modelValue,
  5. 'onUpdate:modelValue': value => this.$emit('onUpdate:modelValue', value)
  6. })
  7. }

v-on

We have to provide a proper prop name for the event handler, e.g., to handle click events, the prop name would be onClick.

  1. render() {
  2. return Vue.h('div', {
  3. onClick: $event => console.log('clicked', $event.target)
  4. })
  5. }

Event Modifiers

For the .passive, .capture, and .once event modifiers, Vue offers object syntax of the handler:

For example:

  1. render() {
  2. return Vue.h('input', {
  3. onClick: {
  4. handler: this.doThisInCapturingMode,
  5. capture: true
  6. },
  7. onKeyUp: {
  8. handler: this.doThisOnce,
  9. once: true
  10. },
  11. onMouseOver: {
  12. handler: this.doThisOnceInCapturingMode,
  13. once: true,
  14. capture: true
  15. },
  16. })
  17. }

For all other event and key modifiers, no special API is necessary, because we can use event methods in the handler:

Modifier(s)Equivalent in Handler
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
Keys:
.enter, .13
if (event.keyCode !== 13) return (change 13 to another key codeRender Functions - 图3 for other key modifiers)
Modifiers Keys:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (change ctrlKey to altKey, shiftKey, or metaKey, respectively)

Here’s an example with all of these modifiers used together:

  1. render() {
  2. return Vue.h('input', {
  3. onKeyUp: event => {
  4. // Abort if the element emitting the event is not
  5. // the element the event is bound to
  6. if (event.target !== event.currentTarget) return
  7. // Abort if the key that went up is not the enter
  8. // key (13) and the shift key was not held down
  9. // at the same time
  10. if (!event.shiftKey || event.keyCode !== 13) return
  11. // Stop event propagation
  12. event.stopPropagation()
  13. // Prevent the default keyup handler for this element
  14. event.preventDefault()
  15. // ...
  16. }
  17. })
  18. }

Slots

You can access slot contents as Arrays of VNodes from this.$slots:

  1. render() {
  2. // `<div><slot></slot></div>`
  3. return Vue.h('div', {}, this.$slots.default())
  4. }
  1. props: ['message'],
  2. render() {
  3. // `<div><slot :text="message"></slot></div>`
  4. return Vue.h('div', {}, this.$slots.default({
  5. text: this.message
  6. }))
  7. }

To pass slots to a child component using render functions:

  1. render() {
  2. // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  3. return Vue.('div', [
  4. Vue.('child', {}, {
  5. // pass `slots` as the children object
  6. // in the form of { name: props => VNode | Array<VNode> }
  7. default: (props) => Vue.h('span', props.text)
  8. })
  9. ])
  10. }

JSX

If we’re writing a lot of render functions, it might feel painful to write something like this:

  1. Vue.h(
  2. 'anchored-heading',
  3. {
  4. level: 1
  5. },
  6. [Vue.h('span', 'Hello'), ' world!']
  7. )

Especially when the template version is so concise in comparison:

  1. <anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>

That’s why there’s a Babel pluginRender Functions - 图4 to use JSX with Vue, getting us back to a syntax that’s closer to templates:

  1. import AnchoredHeading from './AnchoredHeading.vue'
  2. new Vue({
  3. el: '#demo',
  4. render() {
  5. return (
  6. <AnchoredHeading level={1}>
  7. <span>Hello</span> world!
  8. </AnchoredHeading>
  9. )
  10. }
  11. })

For more on how JSX maps to JavaScript, see the usage docsRender Functions - 图5.

Template Compilation

You may be interested to know that Vue’s templates actually compile to render functions. This is an implementation detail you usually don’t need to know about, but if you’d like to see how specific template features are compiled, you may find it interesting. Below is a little demo using Vue.compile to live-compile a template string: