How to Use Render Function in Vue 3 `<script setup>`: A Comprehensive Guide for Developers
Vue 3’s <script setup> syntax has revolutionized how developers write components, offering a more concise and intuitive way to leverage the Composition API. While Vue’s template system is declarative and ideal for most use cases, there are scenarios where you need fine-grained control over the DOM—dynamic component generation, complex conditional logic, or integration with non-Vue libraries, for example. This is where render functions shine.
Render functions are the low-level API that Vue uses under the hood to compile templates into virtual DOM nodes. In Vue 3, the render function API is more flexible and aligned with the Composition API, making it a powerful tool when templates aren’t sufficient.
In this guide, we’ll demystify render functions in Vue 3’s <script setup> syntax. We’ll cover everything from basic structure to advanced use cases, with practical examples to help you master this essential skill.
Table of Contents#
- Understanding Render Functions in Vue 3
- Why Use Render Functions Over Templates?
- Prerequisites
- Setting Up a Vue 3 Project
- Basic Render Function Structure in
<script setup> - Core Concepts: The
h()Function - Creating Elements with
h() - Handling Props and Attributes
- Event Handling
- Conditional Rendering
- List Rendering
- Slots in Render Functions
- Composition with Other Components
- Advanced Use Cases
- Best Practices
- References
1. Understanding Render Functions in Vue 3#
At their core, render functions are JavaScript functions that return a virtual DOM (VNode) tree. Vue’s template syntax is syntactic sugar for render functions—when you write a template, Vue compiles it into a render function during build time.
Render functions give you direct control over how VNodes are created and composed. Unlike templates, which are declarative and limited to Vue’s template syntax, render functions are imperative and let you use full JavaScript to generate UI.
2. Why Use Render Functions Over Templates?#
Templates are preferred for most Vue projects because they’re:
- Declarative: Easier to read and reason about for static or moderately dynamic UIs.
- Optimized: Vue’s template compiler applies optimizations like static hoisting.
However, render functions are better suited for:
- Dynamic Component Trees: When the UI structure depends heavily on runtime data (e.g., generating forms from a JSON schema).
- Programmatic Control: When you need to use JavaScript logic like loops, conditionals, or helper functions to build the UI.
- Integration with External Libraries: Libraries like D3.js or Chart.js often require manual DOM manipulation, which render functions simplify.
- Performance-Critical Sections: For highly dynamic UIs, render functions can sometimes be optimized more granularly than templates.
3. Prerequisites#
Before diving in, ensure you have:
- Basic knowledge of Vue 3 (components, props, events).
- Familiarity with
<script setup>syntax (Vue 3.2+). - Understanding of JavaScript/TypeScript (ES6+ features like arrow functions, destructuring).
- Optional: Familiarity with JSX (we’ll touch on it briefly as an alternative to
h()).
4. Setting Up a Vue 3 Project#
If you don’t have a Vue 3 project, create one using Vite (recommended for speed):
# Create a new Vue 3 project
npm create vite@latest my-render-demo -- --template vue
cd my-render-demo
npm install
npm run devThis will set up a basic Vue 3 project with <script setup> support.
5. Basic Render Function Structure in <script setup>#
In <script setup>, you define a render function by exporting a default function that returns a VNode. To create VNodes, use Vue’s h() (hyperscript) function, which is imported from vue.
Example: Minimal Render Function#
<!-- src/components/RenderDemo.vue -->
<script setup>
import { h } from 'vue'
// Export the render function as default
export default () => {
// Return a VNode created with h()
return h('div', { class: 'greeting' }, 'Hello from Render Function!')
}
</script>Explanation:
h()generates a VNode (virtual DOM node).- The render function is exported as the default, replacing the template.
- When this component is used, it renders a
<div class="greeting">with the text "Hello from Render Function!".
6. Core Concepts: The h() Function#
The h() function is the cornerstone of render functions. It takes three arguments:
h(tag: string | Component, props?: object, children?: VNodeChild | VNodeChild[])Parameters:#
tag: The element tag (e.g.,'div','button') or a Vue component.props(optional): An object containing props, attributes, event handlers, or directives.children(optional): Child nodes (strings, other VNodes, or arrays of VNodes).
7. Creating Elements with h()#
Let’s explore how to build UI elements using h().
7.1 Basic Elements#
Render a native HTML element with text content:
<script setup>
import { h } from 'vue'
export default () => {
return h('h1', { style: { color: 'blue' } }, 'Welcome to My App')
}
</script>Output: A blue <h1> heading with the text "Welcome to My App".
7.2 Nested Elements#
Children can be arrays of h() calls for nested structures:
<script setup>
import { h } from 'vue'
export default () => {
return h('div', { class: 'container' }, [
h('h2', 'User Profile'),
h('p', 'Name: John Doe'),
h('p', 'Age: 30')
])
}
</script>Output: A <div> containing an <h2> and two <p> tags.
8. Handling Props and Attributes#
The props argument in h() lets you pass data to elements or components.
8.1 Native HTML Attributes#
For native elements (e.g., div, input), pass attributes directly:
<script setup>
import { h } from 'vue'
export default () => {
return h('input', {
type: 'text',
placeholder: 'Enter your name',
class: 'input-field' // Attribute
})
}
</script>8.2 Component Props#
For Vue components, pass props using the component’s prop names:
<!-- src/components/Greeting.vue -->
<script setup>
defineProps({
name: { type: String, required: true }
})
</script>
<template>
<p>Hello, {{ name }}!</p>
</template>Now use Greeting in a render function:
<!-- src/components/RenderWithComponent.vue -->
<script setup>
import { h } from 'vue'
import Greeting from './Greeting.vue'
export default () => {
return h(Greeting, { name: 'Alice' }) // Pass prop "name" to Greeting
}
</script>Output: <p>Hello, Alice!</p>.
8.3 Distinguishing Props vs. Attributes#
Vue automatically distinguishes between props (defined with defineProps) and attributes (native HTML attributes). For components, unknown props are treated as attributes and applied to the root element.
9. Event Handling#
To handle events, prefix event names with on (e.g., onClick, onSubmit).
Example: Button Click#
<script setup>
import { h } from 'vue'
export default () => {
const handleClick = () => {
alert('Button clicked!')
}
return h('button', {
class: 'btn',
onClick: handleClick // Event handler
}, 'Click Me')
}
</script>Event Modifiers#
Vue’s event modifiers (e.g., .prevent, .stop) are supported via camelCase suffixes:
| Modifier | Render Function Equivalent |
|---|---|
.prevent | onClickPrevent |
.stop | onClickStop |
.once | onClickOnce |
Example: Form Submit with .prevent
<script setup>
import { h } from 'vue'
export default () => {
const handleSubmit = (e) => {
e.preventDefault() // Optional: modifier already handles this
console.log('Form submitted!')
}
return h('form', {
onSubmitPrevent: handleSubmit // .prevent modifier
}, [
h('button', { type: 'submit' }, 'Submit')
])
}
</script>10. Conditional Rendering#
Use JavaScript conditionals (ternary operators, if/else) to dynamically render content.
Example: Conditional Text#
<script setup>
import { h, ref } from 'vue'
const isLoggedIn = ref(false)
export default () => {
return h('div', null, [
isLoggedIn.value
? h('p', 'Welcome back!')
: h('button', { onClick: () => isLoggedIn.value = true }, 'Log In')
])
}
</script>Behavior: Shows a "Log In" button if isLoggedIn is false; shows a welcome message when clicked.
11. List Rendering#
Use Array.map() to render lists of items. Always include a key prop for optimization.
Example: Render a List#
<script setup>
import { h } from 'vue'
const items = ['Apple', 'Banana', 'Cherry']
export default () => {
return h('ul', null,
items.map((item, index) =>
h('li', { key: index }, item) // key is required for list diffing
)
)
}
</script>Output: A <ul> with three <li> elements for each fruit.
12. Slots in Render Functions#
Slots let parent components pass content to child components. In render functions, access slots via the slots prop.
Example: Component with Slots#
First, define a child component that uses slots:
<!-- src/components/Modal.vue -->
<script setup>
import { h } from 'vue'
// Accept slots as a prop
const props = defineProps({
slots: { type: Object, required: true }
})
export default () => {
return h('div', { class: 'modal' }, [
h('div', { class: 'modal-header' }, props.slots.header?.()), // Named slot: header
h('div', { class: 'modal-body' }, props.slots.default?.()) // Default slot
])
}
</script>Now use the Modal component with slots:
<!-- src/App.vue -->
<script setup>
import { h } from 'vue'
import Modal from './components/Modal.vue'
export default () => {
return h(Modal, {
slots: {
header: () => h('h3', 'Modal Title'), // Named slot
default: () => h('p', 'Modal content here...') // Default slot
}
})
}
</script>13. Composition with Other Components#
Render functions work seamlessly with other Vue components. Import the component and pass it as the tag to h().
Example: Using a Counter Component#
<!-- src/components/Counter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
defineProps({ initialCount: Number })
defineExpose({ count, increment }) // Expose for parent access
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>Now use Counter in a render function:
<script setup>
import { h } from 'vue'
import Counter from './Counter.vue'
export default () => {
return h('div', null, [
h('h4', 'Counter:'),
h(Counter, { initialCount: 5 }) // Pass prop to Counter
])
}
</script>14. Advanced Use Cases#
14.1 Dynamic Component Selection#
Render different components based on runtime data:
<script setup>
import { h } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
const currentRoute = 'home' // Could be dynamic (e.g., from Vue Router)
const components = {
home: Home,
about: About
}
export default () => {
const Component = components[currentRoute]
return h(Component)
}
</script>14.2 JSX (Alternative to h())#
For developers familiar with React, Vue supports JSX via the @vitejs/plugin-vue-jsx plugin. JSX is more readable for complex UIs than nested h() calls.
Setup JSX:
- Install the plugin:
npm install @vitejs/plugin-vue-jsx -D - Update
vite.config.js:import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' export default defineConfig({ plugins: [vue(), vueJsx()] })
JSX Example:
<script setup lang="tsx">
// No need for h()—JSX is transpiled to h() calls
export default () => {
const name = 'Vue'
return <div class="jsx-demo">Hello, {name}!</div>
}
</script>15. Best Practices#
- Prefer Templates When Possible: Templates are declarative and easier to debug for most cases.
- Keep Render Functions Small: Split complex logic into helper functions or composables.
- Use TypeScript: Add type safety to
h()calls and props (e.g.,import type { VNode } from 'vue'). - Avoid Side Effects: Render functions should be pure (no API calls or DOM mutations during rendering).
- Test Thoroughly: Use Vue Test Utils to test dynamic behavior in render functions.
16. References#
- Vue 3 Render Function API Documentation
- Vue 3
<script setup>Documentation - h() Function Type Definition
- JSX in Vue
By mastering render functions in Vue 3’s <script setup>, you unlock powerful ways to build dynamic, flexible UIs. While templates are the go-to for most projects, render functions (and JSX) provide the control needed for complex scenarios. Experiment with the examples above to deepen your understanding! 🚀