core Documentation

Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

54,226 stars TypeScript TypeScript 1 file ยท ~32,950 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy core on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name core --restart always home-assistant/core:latest

1. Project Overview & Quickstart (vuejs/core)

File: README.md

vuejs/core [](https://www.npmjs.com/package/vue) [](https://github.com/vuejs/core/actions/workflows/ci.yml) [](https://www.npmjs.com/package/vue)

Getting Started

Please follow the documentation at vuejs.org!

Sponsors

Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider sponsoring Vue's development.

Questions

For questions and support please use the official forum or community chat. The issue list of this repo is exclusively for bug reports and feature requests.

Issues

Please make sure to respect issue requirements and use the new issue helper when opening an issue. Issues not conforming to the guidelines may be closed immediately.

Stay In Touch

Contribution

Please make sure to read the Contributing Guide before making a pull request. If you have a Vue-related project/component/tool, add it with a pull request to this curated list!

Thank you to all the people who already contributed to Vue!

Note: Showing the first 500 contributors only due to GitHub image size limitations

License

MIT

Copyright (c) 2013-present, Yuxi (Evan) You


File: packages/compiler-sfc/README.md

@vue/compiler-sfc

Lower level utilities for compiling Vue Single File Components

Note: as of 3.2.13+, this package is included as a dependency of the main vue package and can be accessed as vue/compiler-sfc. This means you no longer need to explicitly install this package and ensure its version matches that of vue's. Just use the main vue/compiler-sfc deep import instead.

This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in vue-loader and @vitejs/plugin-vue.

API

The API is intentionally low-level due to the various considerations when integrating Vue SFCs in a build system:

  • Separate hot-module replacement (HMR) for script, template and styles

    • template updates should not reset component state
    • style updates should be performed without component re-render
  • Leveraging the tool's plugin system for pre-processor handling. e.g. <style lang="scss"> should be processed by the corresponding webpack loader.

  • In some cases, transformers of each block in an SFC do not share the same execution context. For example, when used with thread-loader or other parallelized configurations, the template sub-loader in vue-loader may not have access to the full SFC and its descriptor.

The general idea is to generate a facade module that imports the individual blocks of the component. The trick is the module imports itself with different query strings so that the build system can handle each request as "virtual" modules:

text
+--------------------+
                                  |                    |
                                  |  script transform  |
                           +----->+                    |
                           |      +--------------------+
                           |
+--------------------+     |      +--------------------+
|                    |     |      |                    |
|  facade transform  +----------->+ template transform |
|                    |     |      |                    |
+--------------------+     |      +--------------------+
                           |
                           |      +--------------------+
                           +----->+                    |
                                  |  style transform   |
                                  |                    |
                                  +--------------------+

Where the facade module looks like this:

js
// main script
import script from '/project/foo.vue?vue&type=script'
// template compiled to render function
import { render } from '/project/foo.vue?vue&type=template&id=xxxxxx'
// css
import '/project/foo.vue?vue&type=style&index=0&id=xxxxxx'

// attach render function to script
script.render = render

// attach additional metadata
// some of these should be dev only
script.__file = 'example.vue'
script.__scopeId = 'xxxxxx'

// additional tooling-specific HMR handling code
// using __VUE_HMR_API__ global

export default script

High Level Workflow

  1. In facade transform, parse the source into descriptor with the parse API and generate the above facade module code based on the descriptor;

  2. In script transform, use compileScript to process the script. This handles features like <script setup> and CSS variable injection. Alternatively, this can be done directly in the facade module (with the code inlined instead of imported), but it will require rewriting export default to a temp variable (a rewriteDefault convenience API is provided for this purpose) so additional options can be attached to the exported object.

  3. In template transform, use compileTemplate to compile the raw template into render function code.

  4. In style transform, use compileStyle to compile raw CSS to handle <style scoped>, <style module> and CSS variable injection.

Options needed for these APIs can be passed via the query string.

For detailed API references and options, check out the source type definitions. For actual usage of these APIs, check out @vitejs/plugin-vue or vue-loader.


File: packages/reactivity/README.md

@vue/reactivity

Usage Note

This package is inlined into Global & Browser ESM builds of user-facing renderers (e.g. @vue/runtime-dom), but also published as a package that can be used standalone. The standalone build should not be used alongside a pre-bundled build of a user-facing renderer, as they will have different internal storage for reactivity connections. A user-facing renderer should re-export all APIs from this package.

For full exposed APIs, see src/index.ts.

Credits

The implementation of this module is inspired by the following prior art in the JavaScript ecosystem:

Caveats

  • Built-in objects are not observed except for Array, Map, WeakMap, Set and WeakSet.

File: packages/runtime-core/README.md

@vue/runtime-core

This package is published only for typing and building custom renderers. It is NOT meant to be used in applications.

For full exposed APIs, see src/index.ts.

Building a Custom Renderer

ts
import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement,
  // ...
})

// `render` is the low-level API
// `createApp` returns an app instance with configurable context shared
// by the entire app tree.
export { render, createApp }

export * from '@vue/runtime-core'

See @vue/runtime-dom for how a DOM-targeting renderer is implemented.


File: packages/runtime-dom/README.md

@vue/runtime-dom

js
import { h, createApp } from '@vue/runtime-dom'

const RootComponent = {
  render() {
    return h('div', 'hello world')
  },
}

createApp(RootComponent).mount('#app')

File: packages/runtime-test/README.md

@vue/runtime-test

This is for Vue's own internal tests only - it ensures logic tested using this package is DOM-agnostic, and it runs faster than JSDOM.

It can also be used as a reference for implementing a custom renderer.

js
import { h, render, nodeOps, dumpOps } from '@vue/runtime-test'

const App = {
  data() {
    return {
      msg: 'Hello World!',
    }
  },
  render() {
    return h('div', this.msg)
  },
}

// root is of type `TestElement` as defined in src/nodeOps.ts
const root = nodeOps.createElement('div')
render(h(App), root)

const ops = dumpOps()
console.log(ops)

File: packages/server-renderer/README.md

@vue/server-renderer

Note: as of 3.2.13+, this package is included as a dependency of the main vue package and can be accessed as vue/server-renderer. This means you no longer need to explicitly install this package and ensure its version matches that of vue's. Just use the vue/server-renderer deep import instead.

Basic API

`renderToString`

Signature

ts
function renderToString(
  input: App | VNode,
  context?: SSRContext,
): Promise<string>

Usage

js
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')

const app = createSSRApp({
  data: () => ({ msg: 'hello' }),
  template: `{{ msg }}`,
})

;(async () => {
  const html = await renderToString(app)
  console.log(html)
})()

Handling Teleports

If the rendered app contains teleports, the teleported content will not be part of the rendered string. Instead, they are exposed under the teleports property of the ssr context object:

js
const ctx = {}
const html = await renderToString(app, ctx)

console.log(ctx.teleports) // { '#teleported': 'teleported content' }

Streaming API

`renderToNodeStream`

Renders input as a Node.js Readable stream.

Signature

ts
function renderToNodeStream(input: App | VNode, context?: SSRContext): Readable

Usage

js
// inside a Node.js http handler
renderToNodeStream(app).pipe(res)

Note: This method is not supported in the ESM build of @vue/server-renderer, which is decoupled from Node.js environments. Use pipeToNodeWritable instead.

`pipeToNodeWritable`

Render and pipe to an existing Node.js Writable stream instance.

Signature

ts
function pipeToNodeWritable(
  input: App | VNode,
  context: SSRContext = {},
  writable: Writable,
): void

Usage

js
// inside a Node.js http handler
pipeToNodeWritable(app, {}, res)

`renderToWebStream`

Renders input as a Web ReadableStream.

Signature

ts
function renderToWebStream(
  input: App | VNode,
  context?: SSRContext,
): ReadableStream

Usage

js
// inside an environment with ReadableStream support
return new Response(renderToWebStream(app))

Note: in environments that do not expose ReadableStream constructor in the global scope, pipeToWebWritable should be used instead.

`pipeToWebWritable`

Render and pipe to an existing Web WritableStream instance.

Signature

ts
function pipeToWebWritable(
  input: App | VNode,
  context: SSRContext = {},
  writable: WritableStream,
): void

Usage

This is typically used in combination with TransformStream:

js
// TransformStream is available in environments such as CloudFlare workers.
// in Node.js, TransformStream needs to be explicitly imported from 'stream/web'
const { readable, writable } = new TransformStream()
pipeToWebWritable(app, {}, writable)

return new Response(readable)

`renderToSimpleStream`

Renders input in streaming mode using a simple readable interface.

Signature

ts
function renderToSimpleStream(
  input: App | VNode,
  context: SSRContext,
  options: SimpleReadable,
): SimpleReadable

interface SimpleReadable {
  push(content: string | null): void
  destroy(err: any): void
}

Usage

js
let res = ''

renderToSimpleStream(
  app,
  {},
  {
    push(chunk) {
      if (chunk === null) {
        // done
        console.log(`render complete: ${res}`)
      } else {
        res += chunk
      }
    },
    destroy(err) {
      // error encountered
    },
  },
)

2. Official Technical Reference & Guides (vuejs/docs)

File: README.md

vuejs.org

Contributing

This site is built with VitePress and depends on @vue/theme. Site content is written in Markdown format located in src. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.

For local development, pnpm is preferred as package manager:

bash
pnpm i
pnpm run dev

This project requires Node.js to be v20 or higher. And it is recommended to enable corepack:

bash
corepack enable

Working on the content

Working on the theme

If changes need to be made for the theme, check out the instructions for developing the theme alongside the docs.


File: src/guide/extras/animation.md

Animation Techniques {#animation-techniques}

Vue provides the ](/guide/built-ins/transition) and [ components for handling enter / leave and list transitions. However, there are many other ways of using animations on the web, even in a Vue application. Here we will discuss a few additional techniques.

Class-based Animations {#class-based-animations}

For elements that are not entering / leaving the DOM, we can trigger animations by dynamically adding a CSS class:

js
const disabled = ref(false)

function warnDisabled() {
  disabled.value = true
  setTimeout(() => {
    disabled.value = false
  }, 1500)
}
js
export default {
  data() {
    return {
      disabled: false
    }
  },
  methods: {
    warnDisabled() {
      this.disabled = true
      setTimeout(() => {
        this.disabled = false
      }, 1500)
    }
  }
}
vue-html
Click me
  This feature is disabled!
css
.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0);
}

@keyframes shake {
  10%,
  90% {
    transform: translate3d(-1px, 0, 0);
  }

  20%,
  80% {
    transform: translate3d(2px, 0, 0);
  }

  30%,
  50%,
  70% {
    transform: translate3d(-4px, 0, 0);
  }

  40%,
  60% {
    transform: translate3d(4px, 0, 0);
  }
}

State-driven Animations {#state-driven-animations}

Some transition effects can be applied by interpolating values, for instance by binding a style to an element while an interaction occurs. Take this example for instance:

js
const x = ref(0)

function onMousemove(e) {
  x.value = e.clientX
}
js
export default {
  data() {
    return {
      x: 0
    }
  },
  methods: {
    onMousemove(e) {
      this.x = e.clientX
    }
  }
}
vue-html
Move your mouse across this div...
  x: {{ x }}
css
.movearea {
  transition: 0.3s background-color ease;
}

In addition to color, you can also use style bindings to animate transform, width, or height. You can even animate SVG paths using spring physics - after all, they are all attribute data bindings:

Animating with Watchers {#animating-with-watchers}

With some creativity, we can use watchers to animate anything based on some numerical state. For example, we can animate the number itself:

js
import { ref, reactive, watch } from 'vue'
import gsap from 'gsap'

const number = ref(0)
const tweened = reactive({
  number: 0
})

// Note: For inputs greater than Number.MAX_SAFE_INTEGER (9007199254740991),
// the result may be inaccurate due to limitations in JavaScript number precision.
watch(number, (n) => {
  gsap.to(tweened, { duration: 0.5, number: Number(n) || 0 })
})
vue-html
Type a number: 
{{ tweened.number.toFixed(0) }}
js
import gsap from 'gsap'

export default {
  data() {
    return {
      number: 0,
      tweened: 0
    }
  },
  // Note: For inputs greater than Number.MAX_SAFE_INTEGER (9007199254740991),
  // the result may be inaccurate due to limitations in JavaScript number precision.
  watch: {
    number(n) {
      gsap.to(this, { duration: 0.5, tweened: Number(n) || 0 })
    }
  }
}
vue-html
Type a number: 
{{ tweened.toFixed(0) }}

Try it in the Playground

Try it in the Playground


File: src/guide/extras/composition-api-faq.md


outline: deep

Composition API FAQ {#composition-api-faq}

:::tip
This FAQ assumes prior experience with Vue - in particular, experience with Vue 2 while primarily using Options API.
:::

What is Composition API? {#what-is-composition-api}

Composition API is a set of APIs that allows us to author Vue components using imported functions instead of declaring options. It is an umbrella term that covers the following APIs:

  • Reactivity API, e.g. ref() and reactive(), that allows us to directly create reactive state, computed state, and watchers.

  • Lifecycle Hooks, e.g. onMounted() and onUnmounted(), that allow us to programmatically hook into the component lifecycle.

  • Dependency Injection, i.e. provide() and inject(), that allow us to leverage Vue's dependency injection system while using Reactivity APIs.

Composition API is a built-in feature of Vue 3 and Vue 2.7. For older Vue 2 versions, use the officially maintained @vue/composition-api plugin. In Vue 3, it is also primarily used together with the <script setup> syntax in Single-File Components. Here's a basic example of a component using Composition API:

vue
<script setup>
import { ref, onMounted } from 'vue'

// reactive state
const count = ref(0)

// functions that mutate state and trigger updates
function increment() {
  count.value++
}

// lifecycle hooks
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  Count is: {{ count }}
</template>

Despite an API style based on function composition, Composition API is NOT functional programming. Composition API is based on Vue's mutable, fine-grained reactivity paradigm, whereas functional programming emphasizes immutability.

If you are interested in learning how to use Vue with Composition API, you can set the site-wide API preference to Composition API using the toggle at the top of the left sidebar, and then go through the guide from the beginning.

Why Composition API? {#why-composition-api}

Better Logic Reuse {#better-logic-reuse}

The primary advantage of Composition API is that it enables clean, efficient logic reuse in the form of Composable functions. It solves all the drawbacks of mixins, the primary logic reuse mechanism for Options API.

Composition API's logic reuse capability has given rise to impressive community projects such as VueUse, an ever-growing collection of composable utilities. It also serves as a clean mechanism for easily integrating stateful third-party services or libraries into Vue's reactivity system, for example immutable data, state machines, and RxJS.

More Flexible Code Organization {#more-flexible-code-organization}

Many users love that we write organized code by default with Options API: everything has its place based on the option it falls under. However, Options API poses serious limitations when a single component's logic grows beyond a certain complexity threshold. This limitation is particularly prominent in components that need to deal with multiple logical concerns, which we have witnessed first hand in many production Vue 2 apps.

Take the folder explorer component from Vue CLI's GUI as an example: this component is responsible for the following logical concerns:

  • Tracking current folder state and displaying its content
  • Handling folder navigation (opening, closing, refreshing...)
  • Handling new folder creation
  • Toggling show favorite folders only
  • Toggling show hidden folders
  • Handling current working directory changes

The original version of the component was written in Options API. If we give each line of code a color based on the logical concern it is dealing with, this is how it looks:

Notice how code dealing with the same logical concern is forced to be split under different options, located in different parts of the file. In a component that is several hundred lines long, understanding and navigating a single logical concern requires constantly scrolling up and down the file, making it much more difficult than it should be. In addition, if we ever intend to extract a logical concern into a reusable utility, it takes quite a bit of work to find and extract the right pieces of code from different parts of the file.

Here's the same component, before and after the refactor into Composition API:

Notice how the code related to the same logical concern can now be grouped together: we no longer need to jump between different options blocks while working on a specific logical concern. Moreover, we can now move a group of code into an external file with minimal effort, since we no longer need to shuffle the code around in order to extract them. This reduced friction for refactoring is key to the long-term maintainability in large codebases.

Better Type Inference {#better-type-inference}

In recent years, more and more frontend developers are adopting TypeScript as it helps us write more robust code, make changes with more confidence, and provides a great development experience with IDE support. However, the Options API, originally conceived in 2013, was designed without type inference in mind. We had to implement some absurdly complex type gymnastics to make type inference work with the Options API. Even with all this effort, type inference for Options API can still break down for mixins and dependency injection.

This had led many developers who wanted to use Vue with TS to lean towards Class API powered by vue-class-component. However, a class-based API heavily relies on ES decorators, a language feature that was only a stage 2 proposal when Vue 3 was being developed in 2019. We felt it was too risky to base an official API on an unstable proposal. Since then, the decorators proposal has gone through yet another complete overhaul, and finally reached stage 3 in 2022. In addition, class-based API suffers from logic reuse and organization limitations similar to Options API.

In comparison, Composition API utilizes mostly plain variables and functions, which are naturally type friendly. Code written in Composition API can enjoy full type inference with little need for manual type hints. Most of the time, Composition API code will look largely identical in TypeScript and plain JavaScript. This also makes it possible for plain JavaScript users to benefit from partial type inference.

Smaller Production Bundle and Less Overhead {#smaller-production-bundle-and-less-overhead}

Code written in Composition API and <script setup> is also more efficient and minification-friendly than Options API equivalent. This is because the template in a <script setup> component is compiled as a function inlined in the same scope of the <script setup> code. Unlike property access from this, the compiled template code can directly access variables declared inside <script setup>, without an instance proxy in between. This also leads to better minification because all the variable names can be safely shortened.

Relationship with Options API {#relationship-with-options-api}

Trade-offs {#trade-offs}

Some users moving from Options API found their Composition API code less organized, and concluded that Composition API is "worse" in terms of code organization. We recommend users with such opinions to look at that problem from a different perspective.

It is true that Composition API no longer provides the "guard rails" that guide you to put your code into respective buckets. In return, you get to author component code like how you would write normal JavaScript. This means you can and should apply any code organization best practices to your Composition API code as you would when writing normal JavaScript. If you can write well-organized JavaScript, you should also be able to write well-organized Composition API code.

Options API does allow you to "think less" when writing component code, which is why many users love it. However, in reducing the mental overhead, it also locks you into the prescribed code organization pattern with no escape hatch, which can make it difficult to refactor or improve code quality in larger scale projects. In this regard, Composition API provides better long term scalability.

Does Composition API cover all use cases? {#does-composition-api-cover-all-use-cases}

Yes in terms of stateful logic. When using Composition API, there are only a few options that may still be needed: props, emits, name, and inheritAttrs.

:::tip

Since 3.3 you can directly use defineOptions in <script setup> to set the component name or inheritAttrs property

:::

If you intend to exclusively use Composition API (along with the options listed above), you can shave a few kbs off your production bundle via a compile-time flag that drops Options API related code from Vue. Note this also affects Vue components in your dependencies.

Can I use both APIs in the same component? {#can-i-use-both-apis-in-the-same-component}

Yes. You can use Composition API via the setup() option in an Options API component.

However, we only recommend doing so if you have an existing Options API codebase that needs to integrate with new features / external libraries written with Composition API.

Will Options API be deprecated? {#will-options-api-be-deprecated}

No, we do not have any plan to do so. Options API is an integral part of Vue and the reason many developers love it. We also realize that many of the benefits of Composition API only manifest in larger-scale projects, and Options API remains a solid choice for many low-to-medium-complexity scenarios.

Relationship with Class API {#relationship-with-class-api}

We no longer recommend using Class API with Vue 3, given that Composition API provides great TypeScript integration with additional logic reuse and code organization benefits.

Comparison with React Hooks {#comparison-with-react-hooks}

Composition API provides the same level of logic composition capabilities as React Hooks, but with some important differences.

React Hooks are invoked repeatedly every time a component updates. This creates a number of caveats that can confuse even seasoned React developers. It also leads to performance optimization issues that can severely affect development experience. Here are some examples:

  • Hooks are call-order sensitive and cannot be conditional.

  • Variables declared in a React component can be captured by a hook closure and become "stale" if the developer fails to pass in the correct dependencies array. This leads to React developers relying on ESLint rules to ensure correct dependencies are passed. However, the rule is often not smart enough and over-compensates for correctness, which leads to unnecessary invalidation and headaches when edge cases are encountered.

  • Expensive computations require the use of useMemo, which again requires manually passing in the correct dependencies array.

  • Event handlers passed to child components cause unnecessary child updates by default, and require explicit useCallback as an optimization. This is almost always needed, and again requires a correct dependencies array. Neglecting this leads to over-rendering apps by default and can cause performance issues without realizing it.

  • The stale closure problem, combined with Concurrent features, makes it difficult to reason about when a piece of hooks code is run, and makes working with mutable state that should persist across renders (via useRef) cumbersome.

Note: some of the above issues that are related to memoization can be resolved by the upcoming React Compiler.

In comparison, Vue Composition API:

  • Invokes setup() or <script setup> code only once. This makes the code align better with the intuitions of idiomatic JavaScript usage as there are no stale closures to worry about. Composition API calls are also not sensitive to call order and can be conditional.

  • Vue's runtime reactivity system automatically collects reactive dependencies used in computed properties and watchers, so there's no need to manually declare dependencies.

  • No need to manually cache callback functions to avoid unnecessary child updates. In general, Vue's fine-grained reactivity system ensures child components only update when they need to. Manual child-update optimizations are rarely a concern for Vue developers.

We acknowledge the creativity of React Hooks, and it is a major source of inspiration for Composition API. However, the issues mentioned above do exist in its design and we noticed Vue's reactivity model happens to provide a way around them.


File: src/guide/extras/reactivity-in-depth.md


outline: deep

Reactivity in Depth {#reactivity-in-depth}

One of Vueโ€™s most distinctive features is the unobtrusive reactivity system. Component state consists of reactive JavaScript objects. When you modify them, the view updates. It makes state management simple and intuitive, but itโ€™s also important to understand how it works to avoid some common gotchas. In this section, we are going to dig into some of the lower-level details of Vueโ€™s reactivity system.

What is Reactivity? {#what-is-reactivity}

This term comes up in programming quite a bit these days, but what do people mean when they say it? Reactivity is a programming paradigm that allows us to adjust to changes in a declarative manner. The canonical example that people usually show, because itโ€™s a great one, is an Excel spreadsheet:

Here cell A2 is defined via a formula of = A0 + A1 (you can click on A2 to view or edit the formula), so the spreadsheet gives us 3. No surprises there. But if you update A0 or A1, you'll notice that A2 automagically updates too.

JavaScript doesnโ€™t usually work like this. If we were to write something comparable in JavaScript:

js
let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // Still 3

When we mutate A0, A2 does not change automatically.

So how would we do this in JavaScript? First, in order to re-run the code that updates A2, let's wrap it in a function:

js
let A2

function update() {
  A2 = A0 + A1
}

Then, we need to define a few terms:

  • The update() function produces a side effect, or effect for short, because it modifies the state of the program.

  • A0 and A1 are considered dependencies of the effect, as their values are used to perform the effect. The effect is said to be a subscriber to its dependencies.

What we need is a magic function that can invoke update() (the effect) whenever A0 or A1 (the dependencies) change:

js
whenDepsChange(update)

This whenDepsChange() function has the following tasks:

  1. Track when a variable is read. E.g. when evaluating the expression A0 + A1, both A0 and A1 are read.

  2. If a variable is read when there is a currently running effect, make that effect a subscriber to that variable. E.g. because A0 and A1 are read when update() is being executed, update() becomes a subscriber to both A0 and A1 after the first call.

  3. Detect when a variable is mutated. E.g. when A0 is assigned a new value, notify all its subscriber effects to re-run.

How Reactivity Works in Vue {#how-reactivity-works-in-vue}

We can't really track the reading and writing of local variables like in the example. There's just no mechanism for doing that in vanilla JavaScript. What we can do though, is intercept the reading and writing of object properties.

There are two ways of intercepting property access in JavaScript: getter / setters and Proxies. Vue 2 used getter / setters exclusively due to browser support limitations. In Vue 3, Proxies are used for reactive objects and getter / setters are used for refs. Here's some pseudo-code that illustrates how they work:

js{4,9,17,22}
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

:::tip
Code snippets here and below are meant to explain the core concepts in the simplest form possible, so many details are omitted, and edge cases ignored.
:::

This explains a few limitations of reactive objects that we have discussed in the fundamentals section:

  • When you assign or destructure a reactive object's property to a local variable, accessing or assigning to that variable is non-reactive because it no longer triggers the get / set proxy traps on the source object. Note this "disconnect" only affects the variable binding - if the variable points to a non-primitive value such as an object, mutating the object would still be reactive.

  • The returned proxy from reactive(), although behaving just like the original, has a different identity if we compare it to the original using the === operator.

Inside track(), we check whether there is a currently running effect. If there is one, we lookup the subscriber effects (stored in a Set) for the property being tracked, and add the effect to the Set:

js
// This will be set right before an effect is about
// to be run. We'll deal with this later.
let activeEffect

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}

Effect subscriptions are stored in a global WeakMap<target, Map<key, Set<effect>>> data structure. If no subscribing effects Set was found for a property (tracked for the first time), it will be created. This is what the getSubscribersForProperty() function does, in short. For simplicity, we will skip its details.

Inside trigger(), we again lookup the subscriber effects for the property. But this time we invoke them instead:

js
function trigger(target, key) {
  const effects = getSubscribersForProperty(target, key)
  effects.forEach((effect) => effect())
}

Now let's circle back to the whenDepsChange() function:

js
function whenDepsChange(update) {
  const effect = () => {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

It wraps the raw update function in an effect that sets itself as the current active effect before running the actual update. This enables track() calls during the update to locate the current active effect.

At this point, we have created an effect that automatically tracks its dependencies, and re-runs whenever a dependency changes. We call this a Reactive Effect.

Vue provides an API that allows you to create reactive effects: watchEffect(). In fact, you may have noticed that it works pretty similarly to the magical whenDepsChange() in the example. We can now rework the original example using actual Vue APIs:

js
import { ref, watchEffect } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()

watchEffect(() => {
  // tracks A0 and A1
  A2.value = A0.value + A1.value
})

// triggers the effect
A0.value = 2

Using a reactive effect to mutate a ref isn't the most interesting use case - in fact, using a computed property makes it more declarative:

js
import { ref, computed } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)

A0.value = 2

Internally, computed manages its invalidation and re-computation using a reactive effect.

So what's an example of a common and useful reactive effect? Well, updating the DOM! We can implement simple "reactive rendering" like this:

js
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `Count is: ${count.value}`
})

// updates the DOM
count.value++

In fact, this is pretty close to how a Vue component keeps the state and the DOM in sync - each component instance creates a reactive effect to render and update the DOM. Of course, Vue components use much more efficient ways to update the DOM than innerHTML. This is discussed in Rendering Mechanism.

The ref(), computed() and watchEffect() APIs are all part of the Composition API. If you have only been using Options API with Vue so far, you'll notice that Composition API is closer to how Vue's reactivity system works under the hood. In fact, in Vue 3 the Options API is implemented on top of the Composition API. All property access on the component instance (this) triggers getter / setters for reactivity tracking, and options like watch and computed invoke their Composition API equivalents internally.

Runtime vs. Compile-time Reactivity {#runtime-vs-compile-time-reactivity}

Vue's reactivity system is primarily runtime-based: the tracking and triggering are all performed while the code is running directly in the browser. The pros of runtime reactivity are that it can work without a build step, and there are fewer edge cases. On the other hand, this makes it constrained by the syntax limitations of JavaScript, leading to the need of value containers like Vue refs.

Some frameworks, such as Svelte, choose to overcome such limitations by implementing reactivity during compilation. It analyzes and transforms the code in order to simulate reactivity. The compilation step allows the framework to alter the semantics of JavaScript itself - for example, implicitly injecting code that performs dependency analysis and effect triggering around access to locally defined variables. The downside is that such transforms require a build step, and altering JavaScript semantics is essentially creating a language that looks like JavaScript but compiles into something else.

The Vue team did explore this direction via an experimental feature called Reactivity Transform, but in the end we have decided that it would not be a good fit for the project due to the reasoning here.

Reactivity Debugging {#reactivity-debugging}

It's great that Vue's reactivity system automatically tracks dependencies, but in some cases we may want to figure out exactly what is being tracked, or what is causing a component to re-render.

Component Debugging Hooks {#component-debugging-hooks}

We can debug what dependencies are used during a component's render and which dependency is triggering an update using the renderTracked``onRenderTracked and renderTriggered``onRenderTriggered lifecycle hooks. Both hooks will receive a debugger event which contains information on the dependency in question. It is recommended to place a debugger statement in the callbacks to interactively inspect the dependency:

vue
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})
</script>
js
export default {
  renderTracked(event) {
    debugger
  },
  renderTriggered(event) {
    debugger
  }
}

:::tip
Component debug hooks only work in development mode.
:::

The debug event objects have the following type:

ts
type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type:
    | TrackOpTypes /* 'get' | 'has' | 'iterate' */
    | TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
  key: any
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

Computed Debugging {#computed-debugging}

We can debug computed properties by passing computed() a second options object with onTrack and onTrigger callbacks:

  • onTrack will be called when a reactive property or ref is tracked as a dependency.
  • onTrigger will be called when the watcher callback is triggered by the mutation of a dependency.

Both callbacks will receive debugger events in the same format as component debug hooks:

js
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // triggered when count.value is tracked as a dependency
    debugger
  },
  onTrigger(e) {
    // triggered when count.value is mutated
    debugger
  }
})

// access plusOne, should trigger onTrack
console.log(plusOne.value)

// mutate count.value, should trigger onTrigger
count.value++

:::tip
onTrack and onTrigger computed options only work in development mode.
:::

Computed debugging options are only available via the Composition API computed() function.

Watcher Debugging {#watcher-debugging}

Similar to computed(), watchers also support the onTrack and onTrigger options:

js
watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

Watchers declared with the object syntax also support the onTrack and onTrigger options:

js
export default {
  watch: {
    source: {
      handler() {
        // ...
      },
      onTrack(e) {
        debugger
      },
      onTrigger(e) {
        debugger
      }
    }
  }
}

:::tip
onTrack and onTrigger watcher options only work in development mode.
:::

Integration with External State Systems {#integration-with-external-state-systems}

Vue's reactivity system works by deeply converting plain JavaScript objects into reactive proxies. The deep conversion can be unnecessary or sometimes unwanted when integrating with external state management systems (e.g. if an external solution also uses Proxies).

The general idea of integrating Vue's reactivity system with an external state management solution is to hold the external state in a shallowRef. A shallow ref is only reactive when its .value property is accessed - the inner value is left intact. When the external state changes, replace the ref value to trigger updates.

Immutable Data {#immutable-data}

If you are implementing an undo / redo feature, you likely want to take a snapshot of the application's state on every user edit. However, Vue's mutable reactivity system isn't best suited for this if the state tree is large, because serializing the entire state object on every update can be expensive in terms of both CPU and memory costs.

Immutable data structures solve this by never mutating the state objects - instead, it creates new objects that share the same, unchanged parts with old ones. There are different ways of using immutable data in JavaScript, but we recommend using Immer with Vue because it allows you to use immutable data while keeping the more ergonomic, mutable syntax.

We can integrate Immer with Vue via a simple composable:

js
import { produce } from 'immer'
import { shallowRef } from 'vue'

export function useImmer(baseState) {
  const state = shallowRef(baseState)
  const update = (updater) => {
    state.value = produce(state.value, updater)
  }

  return [state, update]
}

Try it in the Playground

State Machines {#state-machines}

State Machine is a model for describing all the possible states an application can be in, and all the possible ways it can transition from one state to another. While it may be overkill for simple components, it can help make complex state flows more robust and manageable.

One of the most popular state machine implementations in JavaScript is XState. Here's a composable that integrates with it:

js
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'

export function useMachine(options) {
  const machine = createMachine(options)
  const state = shallowRef(machine.initialState)
  const service = interpret(machine)
    .onTransition((newState) => (state.value = newState))
    .start()
  const send = (event) => service.send(event)

  return [state, send]
}

Try it in the Playground

RxJS {#rxjs}

RxJS is a library for working with asynchronous event streams. The VueUse library provides the @vueuse/rxjs add-on for connecting RxJS streams with Vue's reactivity system.

Connection to Signals {#connection-to-signals}

Quite a few other frameworks have introduced reactivity primitives similar to refs from Vue's Composition API, under the term "signals":

Fundamentally, signals are the same kind of reactivity primitive as Vue refs. It's a value container that provides dependency tracking on access, and side-effect triggering on mutation. This reactivity-primitive-based paradigm isn't a particularly new concept in the frontend world: it dates back to implementations like Knockout observables and Meteor Tracker from more than a decade ago. Vue Options API and the React state management library MobX are also based on the same principles, but hide the primitives behind object properties.

Although not a necessary trait for something to qualify as signals, today the concept is often discussed alongside the rendering model where updates are performed through fine-grained subscriptions. Due to the use of Virtual DOM, Vue currently relies on compilers to achieve similar optimizations. However, we are also exploring a new Solid-inspired compilation strategy, called Vapor Mode, that does not rely on Virtual DOM and takes more advantage of Vue's built-in reactivity system.

API Design Trade-Offs {#api-design-trade-offs}

The design of Preact and Qwik's signals are very similar to Vue's shallowRef: all three provide a mutable interface via the .value property. We will focus the discussion on Solid and Angular signals.

Solid Signals {#solid-signals}

Solid's createSignal() API design emphasizes read / write segregation. Signals are exposed as a read-only getter and a separate setter:

js
const [count, setCount] = createSignal(0)

count() // access the value
setCount(1) // update the value

Notice how the count signal can be passed down without the setter. This ensures that the state can never be mutated unless the setter is also explicitly exposed. Whether this safety guarantee justifies the more verbose syntax could be subject to the requirement of the project and personal taste - but in case you prefer this API style, you can easily replicate it in Vue:

js
import { shallowRef, triggerRef } from 'vue'

export function createSignal(value, options) {
  const r = shallowRef(value)
  const get = () => r.value
  const set = (v) => {
    r.value = typeof v === 'function' ? v(r.value) : v
    if (options?.equals === false) triggerRef(r)
  }
  return [get, set]
}

Try it in the Playground

Angular Signals {#angular-signals}

Angular is undergoing some fundamental changes by foregoing dirty-checking and introducing its own implementation of a reactivity primitive. The Angular Signal API looks like this:

js
const count = signal(0)

count() // access the value
count.set(1) // set new value
count.update((v) => v + 1) // update based on previous value

Again, we can easily replicate the API in Vue:

js
import { shallowRef } from 'vue'

export function signal(initialValue) {
  const r = shallowRef(initialValue)
  const s = () => r.value
  s.set = (value) => {
    r.value = value
  }
  s.update = (updater) => {
    r.value = updater(r.value)
  }
  return s
}

Try it in the Playground

Compared to Vue refs, Solid and Angular's getter-based API style provide some interesting trade-offs when used in Vue components:

  • () is slightly less verbose than .value, but updating the value is more verbose.
  • There is no ref-unwrapping: accessing values always require (). This makes value access consistent everywhere. This also means you can pass raw signals down as component props.

Whether these API styles suit you is to some extent subjective. Our goal here is to demonstrate the underlying similarity and trade-offs between these different API designs. We also want to show that Vue is flexible: you are not really locked into the existing APIs. Should it be necessary, you can create your own reactivity primitive API to suit more specific needs.


File: src/guide/extras/reactivity-transform.md

Reactivity Transform {#reactivity-transform}

:::danger Removed Experimental Feature
Reactivity Transform was an experimental feature, and has been removed in the latest 3.4 release. Please read about the reasoning here.

If you still intend to use it, it is now available via the Vue Macros plugin.
:::

:::tip Composition-API-specific
Reactivity Transform is a Composition-API-specific feature and requires a build step.
:::

Refs vs. Reactive Variables {#refs-vs-reactive-variables}

Ever since the introduction of the Composition API, one of the primary unresolved questions is the use of refs vs. reactive objects. It's easy to lose reactivity when destructuring reactive objects, while it can be cumbersome to use .value everywhere when using refs. Also, .value is easy to miss if not using a type system.

Vue Reactivity Transform is a compile-time transform that allows us to write code like this:

vue
<script setup>
let count = $ref(0)

console.log(count)

function increment() {
  count++
}
</script>

<template>
  {{ count }}
</template>

The $ref() method here is a compile-time macro: it is not an actual method that will be called at runtime. Instead, the Vue compiler uses it as a hint to treat the resulting count variable as a reactive variable.

Reactive variables can be accessed and re-assigned just like normal variables, but these operations are compiled into refs with .value. For example, the <script> part of the above component is compiled into:

js{5,8}
import { ref } from 'vue'

let count = ref(0)

console.log(count.value)

function increment() {
  count.value++
}

Every reactivity API that returns refs will have a $-prefixed macro equivalent. These APIs include:

These macros are globally available and do not need to be imported when Reactivity Transform is enabled, but you can optionally import them from vue/macros if you want to be more explicit:

js
import { $ref } from 'vue/macros'

let count = $ref(0)

Destructuring with `$()` {#destructuring-with}

It is common for a composition function to return an object of refs, and use destructuring to retrieve these refs. For this purpose, reactivity transform provides the $() macro:

js
import { useMouse } from '@vueuse/core'

const { x, y } = $(useMouse())

console.log(x, y)

Compiled output:

js
import { toRef } from 'vue'
import { useMouse } from '@vueuse/core'

const __temp = useMouse(),
  x = toRef(__temp, 'x'),
  y = toRef(__temp, 'y')

console.log(x.value, y.value)

Note that if x is already a ref, toRef(__temp, 'x') will simply return it as-is and no additional ref will be created. If a destructured value is not a ref (e.g. a function), it will still work - the value will be wrapped in a ref so the rest of the code works as expected.

$() destructure works on both reactive objects and plain objects containing refs.

Convert Existing Refs to Reactive Variables with `$()` {#convert-existing-refs-to-reactive-variables-with}

In some cases we may have wrapped functions that also return refs. However, the Vue compiler won't be able to know ahead of time that a function is going to return a ref. In such cases, the $() macro can also be used to convert any existing refs into reactive variables:

js
function myCreateRef() {
  return ref(0)
}

let count = $(myCreateRef())

Reactive Props Destructure {#reactive-props-destructure}

There are two pain points with the current defineProps() usage in <script setup>:

  1. Similar to .value, you need to always access props as props.x in order to retain reactivity. This means you cannot destructure defineProps because the resulting destructured variables are not reactive and will not update.

  2. When using the type-only props declaration, there is no easy way to declare default values for the props. We introduced the withDefaults() API for this exact purpose, but it's still clunky to use.

We can address these issues by applying a compile-time transform when defineProps is used with destructuring, similar to what we saw earlier with $():

html
<script setup lang="ts">
  interface Props {
    msg: string
    count?: number
    foo?: string
  }

  const {
    msg,
    // default value just works
    count = 1,
    // local aliasing also just works
    // here we are aliasing `props.foo` to `bar`
    foo: bar
  } = defineProps()

  watchEffect(() => {
    // will log whenever the props change
    console.log(msg, count, bar)
  })
</script>

The above will be compiled into the following runtime declaration equivalent:

js
export default {
  props: {
    msg: { type: String, required: true },
    count: { type: Number, default: 1 },
    foo: String
  },
  setup(props) {
    watchEffect(() => {
      console.log(props.msg, props.count, props.foo)
    })
  }
}

Retaining Reactivity Across Function Boundaries {#retaining-reactivity-across-function-boundaries}

While reactive variables relieve us from having to use .value everywhere, it creates an issue of "reactivity loss" when we pass reactive variables across function boundaries. This can happen in two cases:

Passing into function as argument {#passing-into-function-as-argument}

Given a function that expects a ref as an argument, e.g.:

ts
function trackChange(x: Ref<number>) {
  watch(x, (x) => {
    console.log('x changed!')
  })
}

let count = $ref(0)
trackChange(count) // doesn't work!

The above case will not work as expected because it compiles to:

ts
let count = ref(0)
trackChange(count.value)

Here count.value is passed as a number, whereas trackChange expects an actual ref. This can be fixed by wrapping count with $$() before passing it:

diff
let count = $ref(0)
- trackChange(count)
+ trackChange($$(count))

The above compiles to:

js
import { ref } from 'vue'

let count = ref(0)
trackChange(count)

As we can see, $$() is a macro that serves as an escape hint: reactive variables inside $$() will not get .value appended.

Returning inside function scope {#returning-inside-function-scope}

Reactivity can also be lost if reactive variables are used directly in a returned expression:

ts
function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // listen to mousemove...

  // doesn't work!
  return {
    x,
    y
  }
}

The above return statement compiles to:

ts
return {
  x: x.value,
  y: y.value
}

In order to retain reactivity, we should be returning the actual refs, not the current value at return time.

Again, we can use $$() to fix this. In this case, $$() can be used directly on the returned object - any reference to reactive variables inside the $$() call will retain the reference to their underlying refs:

ts
function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // listen to mousemove...

  // fixed
  return $$({
    x,
    y
  })
}

Using `$$()` on destructured props {#using-on-destructured-props}

$$() works on destructured props since they are reactive variables as well. The compiler will convert it with toRef for efficiency:

ts
const { count } = defineProps<{ count: number }>()

passAsRef($$(count))

compiles to:

js
setup(props) {
  const __props_count = toRef(props, 'count')
  passAsRef(__props_count)
}

TypeScript Integration {#typescript-integration}

Vue provides typings for these macros (available globally) and all types will work as expected. There are no incompatibilities with standard TypeScript semantics, so the syntax will work with all existing tooling.

This also means the macros can work in any files where valid JS / TS are allowed - not just inside Vue SFCs.

Since the macros are available globally, their types need to be explicitly referenced (e.g. in a env.d.ts file):

ts
/// <reference types="vue/macros-global" />

When explicitly importing the macros from vue/macros, the type will work without declaring the globals.

Explicit Opt-in {#explicit-opt-in}

:::danger No longer supported in core
The following only applies up to Vue version 3.3 and below. Support has been removed in Vue core 3.4 and above, and @vitejs/plugin-vue 5.0 and above. If you intend to continue using the transform, please migrate to Vue Macros instead.
:::

Vite {#vite}

  • Requires @vitejs/plugin-vue@>=2.0.0
  • Applies to SFCs and js(x)/ts(x) files. A fast usage check is performed on files before applying the transform so there should be no performance cost for files not using the macros.
  • Note reactivityTransform is now a plugin root-level option instead of nested as script.refSugar, since it affects not just SFCs.
js [vite.config.js]
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

`vue-cli` {#vue-cli}

  • Currently only affects SFCs
  • Requires vue-loader@>=17.0.0
js [vue.config.js]
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => {
        return {
          ...options,
          reactivityTransform: true
        }
      })
  }
}

Plain `webpack` + `vue-loader` {#plain-webpack-vue-loader}

  • Currently only affects SFCs
  • Requires vue-loader@>=17.0.0
js [webpack.config.js]
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          reactivityTransform: true
        }
      }
    ]
  }
}

File: src/guide/extras/render-function.md


outline: deep

Render Functions & JSX {#render-functions-jsx}

Vue recommends using templates to build applications in the vast majority of cases. However, there are situations where we need the full programmatic power of JavaScript. That's where we can use the render function.

If you are new to the concept of virtual DOM and render functions, make sure to read the Rendering Mechanism chapter first.

Basic Usage {#basic-usage}

Creating Vnodes {#creating-vnodes}

Vue provides an h() function for creating vnodes:

js
import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h() is short for hyperscript - which means "JavaScript that produces HTML (hypertext markup language)". This name is inherited from conventions shared by many virtual DOM implementations. A more descriptive name could be createVNode(), but a shorter name helps when you have to call this function many times in a render function.

The h() function is designed to be very flexible:

js
// all arguments except the type are optional
h('div')
h('div', { id: 'foo' })

// both attributes and properties can be used in props
// Vue automatically picks the right way to assign it
h('div', { class: 'bar', innerHTML: 'hello' })

// props modifiers such as `.prop` and `.attr` can be added
// with `.` and `^` prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })

// class and style have the same object / array
// value support that they have in templates
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// event listeners should be passed as onXxx
h('div', { onClick: () => {} })

// children can be a string
h('div', { id: 'foo' }, 'hello')

// props can be omitted when there are no props
h('div', 'hello')
h('div', [h('span', 'hello')])

// children array can contain mixed vnodes and strings
h('div', ['hello', h('span', 'hello')])

The resulting vnode has the following shape:

js
const vnode = h('div', { id: 'foo' }, [])

vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

:::warning Note
The full VNode interface contains many other internal properties, but it is strongly recommended to avoid relying on any properties other than the ones listed here. This avoids unintended breakage in case the internal properties are changed.
:::

Declaring Render Functions {#declaring-render-functions}

When using templates with Composition API, the return value of the setup() hook is used to expose data to the template. When using render functions, however, we can directly return the render function instead:

js
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // return the render function
    return () => h('div', props.msg + count.value)
  }
}

The render function is declared inside setup() so it naturally has access to the props and any reactive state declared in the same scope.

In addition to returning a single vnode, you can also return strings or arrays:

js
export default {
  setup() {
    return () => 'hello world!'
  }
}
js
import { h } from 'vue'

export default {
  setup() {
    // use an array to return multiple root nodes
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

:::tip
Make sure to return a function instead of directly returning values! The setup() function is called only once per component, while the returned render function will be called multiple times.
:::

We can declare render functions using the render option:

js
import { h } from 'vue'

export default {
  data() {
    return {
      msg: 'hello'
    }
  },
  render() {
    return h('div', this.msg)
  }
}

The render() function has access to the component instance via this.

In addition to returning a single vnode, you can also return strings or arrays:

js
export default {
  render() {
    return 'hello world!'
  }
}
js
import { h } from 'vue'

export default {
  render() {
    // use an array to return multiple root nodes
    return [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

If a render function component doesn't need any instance state, they can also be declared directly as a function for brevity:

js
function Hello() {
  return 'hello world!'
}

That's right, this is a valid Vue component! See Functional Components for more details on this syntax.

Vnodes Must Be Unique {#vnodes-must-be-unique}

All vnodes in the component tree must be unique. That means the following render function is invalid:

js
function render() {
  const p = h('p', 'hi')
  return h('div', [
    // Yikes - duplicate vnodes!
    p,
    p
  ])
}

If you really want to duplicate the same element/component many times, you can do so with a factory function. For example, the following render function is a perfectly valid way of rendering 20 identical paragraphs:

js
function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

Using Vnodes in `