Render functions are an alternative to templates. Instead of writing HTML-like markup, you use the h() function (or JSX) to create vnodes programmatically. Templates are compiled into render functions at build time, so render functions are what Vue actually executes.
The h() function
h stands for "hyperscript" (JavaScript that creates HTML). It takes a tag (or component), props, and children:
import { h, ref } from 'vue'
export default {
setup() {
const count = ref(0)
return () => h('button', {
onClick: () => count.value++
}, `Count: ${count.value}`)
}
}When setup returns a function instead of an object, that function is the render function.
JSX (the readable alternative)
JSX makes render functions look closer to templates. It requires @vitejs/plugin-vue-jsx:
import { ref, defineComponent } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
return () => (
<button onClick={() => count.value++}>
Count: {count.value}
</button>
)
}
})When render functions make sense
Templates handle 95% of cases. Render functions are useful when the output is too dynamic for template syntax:
import { h } from 'vue'
// Dynamic heading level: h1, h2, h3...
function DynamicHeading(props: { level: number }, { slots }) {
return h(`h${props.level}`, slots.default?.())
}
DynamicHeading.props = ['level']// Same component in JSX
function DynamicHeading(props: { level: number }, { slots }) {
const Tag = `h${props.level}`
return <Tag>{slots.default?.()}</Tag>
}Doing this in a template would require a v-if chain for each heading level.
Key patterns
Lists need keys, just like in templates:
return () => h('ul',
items.value.map(item => h('li', { key: item.id }, item.name))
)Event modifiers use withModifiers and withKeys:
import { h, withModifiers, withKeys } from 'vue'
h('button', {
onClick: withModifiers(handleClick, ['stop', 'prevent'])
}, 'Click')
h('input', {
onKeyup: withKeys(handleEnter, ['enter'])
})v-model is expanded manually:
h(CustomInput, {
modelValue: text.value,
'onUpdate:modelValue': (val) => { text.value = val }
})Custom directives use withDirectives:
import { h, withDirectives } from 'vue'
const vFocus = { mounted: (el) => el.focus() }
withDirectives(h('input'), [[vFocus]])Templates vs render functions
| Templates | Render functions / JSX | |
|---|---|---|
| Readability | HTML-like, familiar | JavaScript, more verbose |
| Compiler optimizations | Static hoisting, patch flags | None (you opt out) |
| Dynamic output | Limited by directive syntax | Full JavaScript flexibility |
| IDE support | Vue-specific tooling (Volar) | Standard TypeScript/JSX |
| Use case | Most components | Highly dynamic rendering logic |
Prefer templates by default. Use render functions when the template would be awkward or impossible to express declaratively.