The h Function

What a template compiles to: h() builds vnodes by hand, and setup returns the render function that calls it.

Syntaxh(type: string | Component, props?: object | null, children?: Children | Slot | Slots): VNode

A template is not sent to the browser as HTML. Vue's compiler turns it into a render function — a function that returns a description of the DOM you want. h() is how you build that description by hand. The name is short for hyperscript.

Each call returns a vnode (virtual node): a plain object describing one element, component or piece of text. Vue compares this render's vnodes against the previous ones and patches only what differs.

How it works

The signature is h(type, props, children). type is a tag name string or an imported component. props holds attributes, DOM properties, class, style and event listeners named onClick, onInput and so on. children is a string, a number, a vnode, or an array of those. Both props and children are optional.

A component built this way returns the render function from setup: return () => h(...). Vue re-runs that returned function when reactive state it read during the last call changes.

What trips people up

<script setup> has no way to return a render function, so these components live in a .ts or .tsx file, or in a plain <script> block using defineComponent. The children of a component vnode must be slot functions, not bare vnodes. A vnode may be used once, so to show the same markup twice, call h() twice. For element references, pass ref: 'name' and read it with useTemplateRef, added in Vue 3.5.

Example

Example · vue
// src/components/CounterButton.ts
import { defineComponent, h, ref } from 'vue'

export default defineComponent(
  (props: { label: string }) => {
    const count = ref(0)

    return () =>
      h(
        'button',
        {
          type: 'button',
          class: ['counter', { 'counter-active': count.value > 0 }],
          onClick: () => {
            count.value += 1
          }
        },
        `${props.label}: ${count.value}`
      )
  },
  {
    props: {
      label: { type: String, required: true }
    }
  }
)

When to use it

  • Render a heading whose tag is h1 to h6 depending on how deep the section is nested.
  • Build a table whose column set comes from data rather than being written out in markup.
  • Read someone else's component library, whose source is render functions rather than templates.

More examples

The shape of a vnode

h() has no side effects: it returns a plain object. Reading the object is the quickest way to see that props and children are exactly what you passed, and that key is null until you set one. Keys matter once a vnode sits in a list.

Example · ts
// src/lib/inspect-vnode.ts
import { h } from 'vue'

const vnode = h('div', { id: 'foo' }, 'hello')

console.log(vnode.type)       // Output: div
console.log(vnode.props?.id)  // Output: foo
console.log(vnode.children)   // Output: hello
console.log(vnode.key)        // Output: null

Every argument form h() accepts

Props can be omitted whenever the second argument is clearly children. The one case that is not interchangeable is the last line: when children is a slots object, the props argument must be present, even as null. Event listeners are ordinary props named onXxx.

Example · ts
// src/lib/vnode-forms.ts
import { h } from 'vue'
import UserCard from '../components/UserCard.vue'

const handleSave = () => {}

const rule = h('hr')
const text = h('p', 'text only')
const styled = h('p', { class: ['note', { urgent: true }] }, 'both')
const list = h('ul', [h('li', { key: 'a' }, 'one'), h('li', { key: 'b' }, 'two')])
const button = h('button', { type: 'button', onClick: handleSave }, 'Save')
const card = h(UserCard, { name: 'Ada' }, { footer: () => h('small', 'joined 2019') })

export const all = [rule, text, styled, list, button, card]

Reading slots in a render function

In a render function, slots is a bag of functions on the setup context. Calling slots.default() returns the consumer's vnodes; checking slots.footer before calling it is how you render a wrapper only when that slot was filled. A card with a heading, a body from the default slot, and a footer only when one was passed.

Example · ts
// src/components/CardShell.ts
import { defineComponent, h } from 'vue'

export default defineComponent(
  (props: { title: string }, { slots }) => {
    return () =>
      h('section', { class: 'card' }, [
        h('header', { class: 'card-head' }, props.title),
        h('div', { class: 'card-body' }, slots.default?.()),
        slots.footer ? h('footer', { class: 'card-foot' }, slots.footer()) : null
      ])
  },
  {
    props: {
      title: { type: String, required: true }
    }
  }
)

Discussion

  • Be the first to comment on this lesson.