skip to content

Render Functions & JSX

h() creates vnodes directly, and setup() can return a render function instead of using a template, in plain JS or JSX/TSX. Interviewers ask when a render function beats a template.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Vue 3, what arguments does h() take, and how does a Composition API component return a render function instead of a template?

level: juniorimportance: should knowfreq 38%

answer

  1. type, props, children
  2. flat props, onXxx listeners
  3. setup runs once
  4. return a function, not a vnode

basics

~20 s

h(type, props?, children?) creates a vnode from a tag string or component, flat props with onXxx listeners, and children. setup() returns a function, not a vnode; that function re-runs on every render while setup runs once.

solid answer

~40 s

`h()` is imported from `vue` and takes a `type` (tag string or component definition), an optional flat `props` object and optional `children`. Props mix attributes and DOM properties, `class`/`style` take the template's object and array forms, and listeners are `onClick`-style props; with two arguments Vue decides whether the second is props or children. To skip the template, `setup()` returns a function such as `() => h('div', props.msg + count.value)`. Because `setup()` runs once and the returned function runs on every render, all reactive reads must happen inside the function. It can return a vnode, a string or an array, and every vnode in the tree must be a fresh object.

code

ts · 15 lines
ts
import { defineComponent, h, ref } from 'vue'

export default defineComponent({
  props: { label: { type: String, required: true } },
  setup(props) {
    const count = ref(0)
    // setup runs once; this function runs on every render
    return () =>
      h(
        'button',
        { class: ['btn', { active: count.value > 0 }], onClick: () => count.value++ },
        `${props.label}: ${count.value}`
      )
  }
})

go deeper

for a junior

Recall the three arguments of h(), that listeners are onXxx props, and that setup() can return a function that Vue uses as the render function.

for a middle

Explain why reactive reads must sit inside the returned function, how h() tells props from children, and why vnodes must be unique.

for a senior

Spot Vue 2 render-function code during an upgrade review, and decide when a component is clearer as a render function than as a template.

for a principal

Set a team convention for when render functions are allowed, weighing flexibility against readability and the compiler hints templates provide.

## What `h()` is `h()` (short for *hyperscript*) is the function Vue 3 exports from `vue` for creating **vnodes**: plain JavaScript objects that describe one node of the virtual DOM. A template is compiled into code that creates vnodes; a **render function** is you writing that code by hand. The docs note that a more descriptive name would be `createVNode()`, but a short name helps when you call it many times. ## The signature ```ts h(type, props?, children?) h(type, children?) // props omitted ``` | Argument | Accepts | Notes | |---|---|---| | `type` | a tag string (`'div'`) or a component definition | imported components are passed directly, no registration needed | | `props` | an object, or `null` | attributes, DOM properties, `class`, `style`, listeners, `key`, `ref` | | `children` | a string, a vnode, an array, or slot functions for components | arrays may mix strings and vnodes | The props object is **flat**, which is the main difference from Vue 2: - attributes and DOM properties sit side by side (`{ id: 'foo', innerHTML: 'hi' }`) and Vue decides how to set each one; the `.` and `^` prefixes force property or attribute binding; - `class` and `style` accept the same object and array forms as in templates; - event listeners are props named `on` plus a capitalized event name, such as `onClick`, the equivalent of `@click`; - every argument after `type` is optional, and with two arguments Vue works out whether the second one is props or children. ## Returning a render function from `setup()` With the Composition API, `setup()` normally returns an object whose bindings the template uses. A component without a template can instead **return a function**, and Vue uses that function as the component's render function: ```ts import { defineComponent, h, ref } from 'vue' export default defineComponent({ props: { msg: String }, setup(props) { const count = ref(1) return () => h('div', `${props.msg} ${count.value}`) } }) ``` Two rules make this work: 1. **`setup()` runs once per component instance**, while the returned function runs on every render. Everything that must stay current, including every `.value` read and every `props.x` read, belongs *inside* the returned function. 2. The render function runs inside the component's render effect, so the reactive values it reads are tracked, and changing them schedules a re-render of that component. The function may return a single vnode, a string, or an array for multiple root nodes. A `<script setup>` block cannot return a render function, so this style lives in a plain `setup()` option, or in `defineComponent()`'s function signature (Vue 3.3+), which takes the setup function directly and supports generics for TSX. ## Vnodes must be unique Every vnode in a rendered tree must be a distinct object. Rendering one vnode object in two positions is invalid per the Vue docs: a vnode describes *one* position, and once mounted it holds that position's DOM element and, for a component, its instance. The fix is a factory: ```ts h('div', Array.from({ length: 20 }).map(() => h('p', 'hi'))) ``` ## When Vue suggests reaching for it The Vue docs recommend templates by default: they are closer to HTML, and their deterministic syntax is easier to analyze statically, which lets the template compiler apply compile-time optimizations. Render functions are described as typically used in **reusable components with highly dynamic rendering logic**, where working with vnodes in plain JavaScript beats expressing the same logic in directives. In an interview, name a concrete case (an element whose tag is data-driven, a component that wraps or reorders its slot content) rather than "when I prefer JavaScript". ## Helpers that sit next to `h()` - `mergeProps()` combines several props objects with Vue's merging rules for `class`, `style` and listeners. - `cloneVNode()` copies a vnode, optionally merging in extra props. - `isVNode()` tells whether a value is a vnode. - `withModifiers()` wraps a listener to apply event modifiers such as `self`. ## Common mistakes - Building vnodes in the `setup()` body and returning `() => savedVnode`: the tree is frozen at its first state. - Passing Vue 2-style nested data (`{ on: { click } }`, `{ attrs: {...} }`, `{ domProps: {...} }`): Vue 3 reads it as ordinary props. - Expecting `h` as a parameter (`render(h)`): Vue 3 imports it from `vue`. - Reusing a vnode object in several places instead of creating one per position.

  • Why is it wrong to build the vnode in the setup() body and return () => thatVnode?
    `setup()` runs once per instance, so the vnode captures the values from that single moment. The returned function would hand back the same frozen object on every render and never read `count.value` or `props` again, so nothing updates. Create vnodes inside the returned function so each render reads current state.
  • How do you render the same paragraph twenty times with h()?
    Create twenty vnodes rather than reusing one: `h('div', Array.from({ length: 20 }).map(() => h('p', 'hi')))`. The Vue docs call a render function that places one vnode object in two positions invalid, because each vnode describes one position in the tree and, once mounted, holds that position's DOM element.

saying these in an interview costs you the question

  • Passes listeners as { on: { click } } the way Vue 2 did
  • Expects h to arrive as the render function's first parameter
  • Builds the vnode in setup() and returns it from a closure
  • Believes the render function returned from setup runs only once
  • Reuses one vnode object in several places of the tree
open as a page

In Vue 3, how would you write a heading component that renders <h1> to <h6> from a level prop using a render function?

level: middleimportance: should knowfreq 30%

basics

~20 s

Return a render function from setup() that calls h(h${props.level}, slots.default?.()). The tag is a plain string, so it follows the prop on every render, and calling the slot inside the render function keeps the heading text current.

open as a page

How does writing JSX or TSX in Vue 3 differ from React JSX in attributes, events, children and TypeScript setup?

level: middleimportance: should knowfreq 28%

basics

~20 s

Vue compiles JSX with its own transform into Vue vnodes: you write class and for, events are onXxx props, and a component's children are slot functions or an object of them. TSX needs jsx: preserve and, since 3.4, jsxImportSource: vue.

open as a page

In a Vue 3 render function, how do you pass default, named and scoped slots to a child component, and why must they be functions?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Pass a function for the default slot, or null props plus an object of slot functions for named ones. Scoped slots are functions with a parameter. Functions let the child call them lazily, so the child tracks their dependencies.

open as a page

A Vue 3 render function uses h('ButtonCounter') and h('input', { 'v-focus': true }) for a registered component and directive, but neither works: why, and how do you fix it?

level: seniorimportance: nice to knowfreq 16%

basics

~10 s

A string passed to h() is always a native tag, and v-focus in props is just an attribute. Use resolveComponent('ButtonCounter') or an import for the component, and withDirectives(h('input'), [[focusDirective]]) with resolveDirective('focus') for the directive.

open as a page