In Vue 3, what arguments does h() take, and how does a Composition API component return a render function instead of a template?
answer
- type, props, children
- flat props, onXxx listeners
- setup runs once
- return a function, not a vnode
basics
~20 sh(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 linesimport { 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
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.
Explain why reactive reads must sit inside the returned function, how h() tells props from children, and why vnodes must be unique.
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.
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