99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

Vue Composition API Core Patterns with TypeScript

Vue Composition API Core Patterns with TypeScript

This post assumes you have learned the basic syntax of Vue 3, and it is organized so you can quickly explore the core patterns.
It gathers scattered key information and provides new examples that differ from the official docs.
It is also based on the Vite 'Vue > TypeScript' template and uses the Composition API and TypeScript.

This post is based on Vue 3.5.
Features that need a specific version or later are marked like 3.5+.

# Composition vs Options

The Options API uses required options such as data and methods, so it is easy to learn and use even if you don't know JavaScript well.
It is also good for staying compatible with existing code from Vue 2 and earlier.

The Composition API is a newer script syntax with a more flexible structure that improves code reusability.
It also works well with TypeScript. (You can use the Options API with TypeScript too, but some parts are inconvenient.)

You can use both approaches together in one project.
If you use TypeScript, we recommend the Composition API.

# Creating an app

/index.html
HTML
1
2
<div id="app"></div> <script type="module" src="/src/main.ts"></script>
/src/main.ts
JS
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) // Returns the app instance! app.use(plugin) // Register a plugin! app.component(component) // Register a global component! app.directive(directive) // Register a directive! // ... app.mount('#app') // Call it last!

With method chaining,

JS
1
2
3
4
createApp(App) .use(plugin) .component(component) .mount('#app')

# App instance members

Member Description Note
app.mount() Mounts the app instance to an element
app.unmount() Unmounts the app instance from its element
app.onUnmount() Registers a cleanup callback to call when the app instance is unmounted 3.5+
app.use() Installs an external Vue plugin
app.component() Registers a global component
app.directive() Registers a global custom directive
app.provide() Provides data to inject globally
app.mixin() Applies a global mixin For backward compatibility; direct use is not recommended
app.runWithContext() Runs an immediately invoked callback that can read data provided globally with app.provide() 3.3+
app.version Checks the Vue.js version
app.config Configuration object that holds the app settings
app.config.errorHandler Defines a handler to run when an uncaught error occurs
app.config.warnHandler Defines a handler to run when a warning occurs
app.config.performance Enables performance tracing in development mode
app.config.compilerOptions Sets runtime compiler options
app.config.globalProperties Registers global properties that every child component in the app can access
app.config.optionMergeStrategies Defines custom merge functions for component options
app.config.idPrefix Sets the prefix of IDs generated with useId() 3.5+
app.config.throwUnhandledErrorInProduction Forces unhandled errors to be thrown even in production mode 3.5+

# SFC structure

A *.vue file is a Single File Component (SFC) made up of <template>, <script>, and <style> tags.
A module compiler turns *.vue files into JavaScript files that browsers can use.
With the lang attribute, you can set the language (TypeScript, SCSS, etc.) used in the <script> and <style> tags.

VUE
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts"> // Script (JS) </script> <template> <!-- Template (HTML) --> </template> <style scoped lang="scss"> /* Style (CSS) */ </style>

# Scoped CSS

If you add the scoped attribute to the <style> tag of an SFC component, you can write CSS selectors that match only inside the component.
They don't affect outside components, so you can prevent selector name conflicts between components.
In most cases, we recommend using the scoped attribute.

VUE
1
2
3
4
5
6
7
8
9
<template> <div></div> </template> <style scoped> div { color: red; } </style>

If you check the output, you can see that an attribute starting with data-v- has been added to the element and the selector.
This is called a style hash, and this unique attribute name prevents selector name conflicts between components.

HTML
1
2
3
4
5
6
<style> div[data-v-7a7a37b1] { color: red; } </style> <div data-v-7a7a37b1></div>
Output

The scoped attribute prevents name conflicts, but sometimes you need to affect a nested child component.
In that case, you can use the :deep() pseudo-class.

/src/components/Parent.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts"> import Child from './components/Child.vue' </script> <template> <div class="parent"> <Child /> .child </div> </template> <style scoped> .parent :deep(.child) { color: red; } </style>
/src/components/Child.vue
VUE
1
2
3
4
5
6
7
<template> <div class="child-wrap"> <div class="child">CHILD</div> </div> </template> <style scoped></style>

The style hash that the parent component gets from the scoped attribute is applied to the root element of the child component.

HTML
1
2
3
4
5
6
7
<div data-v-7a7a37b1 class="parent"> <div data-v-7a7a37b1 data-v-0904fc8e class="child-wrap"> <div data-v-0904fc8e class="child"> CHILD </div> </div> </div>
Output

You can use several <style> tags in one component (SFC).

VUE
1
2
3
<style scoped lang="scss"></style> <style scoped></style> <style></style>

# Reactive data

When data is shown on the screen, changing the data does not update the screen by default.
In modern frontend frameworks like Vue, the feature that automatically updates the screen when data changes is called 'reactivity', and data with that reactivity is called 'reactive state' or 'reactive' for short.

# ref vs reactive

The ref() and reactive() functions both return reactive data, but they differ in a few ways.
The ref() function can be used with any data type,
while the reactive() function is reactive only when you use it with reference types such as objects or arrays.

Function Primitive reactivity Reference reactivity Uses .value Returns Implicit deep watch
ref() O O O Ref object X
reactive() X O X Reactive data O

ref() is used more often, but it is a bit tedious to access the value through the .value property every time.
The reactive() function can be reactive without the .value property, but it can be used only with reference types.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup> import { ref, reactive } from 'vue' const refPrimitive = ref(123) // Reactive const reactivePrimitive = reactive(123) // Not reactive console.log(refPrimitive.value) // 123 console.log(reactivePrimitive === 123) // true const user = { name: 'HEROPY', age: 85 } const refUser = ref(user) const reactiveUser = reactive(user) console.log(refUser.value.name) // 'HEROPY' console.log(reactiveUser.name) // 'HEROPY' console.log(refUser.value.age) // 85 console.log(reactiveUser.age) // 85 </script>

In the template, you don't use the .value property with either ref() or reactive().

VUE
1
2
3
4
5
6
7
8
9
10
<template> <div>{{ refPrimitive }}</div> <div>{{ reactivePrimitive }}</div> <div>{{ refUser.name }}</div> <div>{{ reactiveUser.name }}</div> <div>{{ refUser.age }}</div> <div>{{ reactiveUser.age }}</div> </template>

With TypeScript, you can spot the problem of declaring a primitive with reactive() right away.

VUE
1
2
3
4
5
<script setup lang="ts"> import { reactive } from 'vue' const reactivePrimitive = reactive(123) // Argument of type 'number' is not assignable to parameter of type 'object'.ts(2345) </script>

# Template syntax

This is the syntax you use inside the root <template> tag (HTML) of a *.vue component.

# Data interpolation

Top-level variables in the <script> tag of a *.vue component can be interpolated into the template, whether or not they are reactive.
The pattern of interpolating data with curly braces ({}), like {{ data }}, is called the 'double curly brace syntax (Mustache)'.

VUE
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts"> import { ref } from 'vue' const data = 123 // Plain data const reactiveData = ref(123) // Reactive data </script> <template> <div>{{ data }}</div> <div>{{ reactiveData }}</div> </template>
Double curly brace data interpolation
HTML
1
2
<div>123</div> <div>123</div>
Output

# Rendering HTML

To actually render HTML code contained in a string, you need to use the v-html directive instead of double curly braces.

Special attributes that start with the v- prefix, such as v-html and v-bind, are called directives.

VUE
1
2
3
<script setup lang="ts"> const rawHtml = '<div style="color: red;">Rendered HTML</div>' </script>
A string that contains HTML code

If you simply output an HTML code string with the double curly brace syntax, it is output as plain text, like el.textContent.
With the v-html directive, it is output like el.innerHTML.

VUE
1
2
3
4
<template> <div>{{ rawHtml }}</div> <div v-html="rawHtml"></div> </template>
VUE
1
2
<div>&lt;div style="color: red;"&gt;Rendered HTML&lt;/div&gt;</div> <div style="color: red;">Rendered HTML</div>
Output

Dynamically rendering HTML written by outside users (such as in a comment system) can cause XSS vulnerabilities (Korean), so be careful, but with trusted content, it's perfectly fine to use!

# Attribute bindings

You can bind data to HTML attributes.
The :attribute pattern (shorthand) is recommended over the v-bind:attribute pattern because it is shorter and cleaner.
To bind several attributes at once, use an attribute object as the value of v-bind.

1
2
3
<element v-bind:attribute="data" /> <element :attribute="data" /> <element v-bind="attribute_object" />
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { ref } from 'vue' const name = ref('hello-world') const isValid = ref(true) const attrs = ref({ id: 'hello-world', disabled: true, }) </script> <template> <!-- Regular attribute binding --> <div :id="name"></div> <!-- Boolean attribute binding --> <input :disabled="isValid" /> <!-- Binding several attributes with an object --> <button v-bind="attrs">Button!</button> </template>
HTML
1
2
3
4
5
<div id="hello-world"></div> <input disabled /> <button id="hello-world" disabled>Button!</button>
Output

With the :['attributeName'] pattern, you can decide the attribute name dynamically.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup lang="ts"> import { ref } from 'vue' const attrName = ref('id') const attrValue = ref('hello') </script> <template> <button @click="attrName = attrName === 'id' ? 'class' : 'id'">Toggle attribute name!</button> <h1 :[attrName]="attrValue">Hello!</h1> </template> <style scoped> #hello { color: red; } .hello { color: blue; } </style>
Deciding between the id and class attributes dynamically

# Using expressions

You can use JavaScript expressions in double curly braces and in every directive.
For example, you can't use declarations such as const a = 1 or function b() {}.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts"> import { ref } from 'vue' const name = ref('HEROPY') const age = ref(123) const isValid = ref(true) function add(a: number, b: number) { return a + b } </script> <template> <div>{{ name.split('').reverse().join('') }}</div> <div>{{ age - 100 }}</div> <div>{{ isValid ? 'YES' : 'NO' }}</div> <div :id="`hello-${name}`">{{ add(age, 85) }}</div> </template>

# HTML class bindings

By binding object and array data, you can dynamically add and remove HTML class selectors.
In general, binding with an object literal is the common approach.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script setup lang="ts"> import { ref } from 'vue' const isActive = ref(true) const error = ref(false) const status = ref('primary') const cssClasses = ref({ active: true, error: false }) </script> <template> <!-- Binding with an object --> <div class="btn" :class="{ active: isActive, error, [`btn-${status}`]: true, }"></div> <!-- Binding with an array --> <div class="btn" :class="[ isActive ? 'active' : '', error ? 'error' : '', `btn-${status}`, ]"></div> <!-- Using an object and an array together --> <div class="btn" :class="[ cssClasses, `btn-${status}`, ]"></div> </template>
HTML
1
2
3
<div class="btn active btn-primary"></div> <div class="btn active btn-primary"></div> <div class="btn active btn-primary"></div>
Output

# Inline style bindings

By binding object and array data, you can dynamically add and remove CSS properties and values.
If you write property names with two or more words, such as font-size and background-color, in camel case, they are automatically converted to kebab case.
When you use number data, you must write the unit with it.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup lang="ts"> import { ref } from 'vue' const color = ref('red') const size = ref(16) const colorStyles = { color: 'red', backgroundColor: 'blue', } </script> <template> <!-- Binding with an object --> <div :style="{ color, fontSize: `${size}px`, backgroundColor: colorStyles.backgroundColor }"></div> <!-- Binding with an array --> <!-- A pattern for using several style objects --> <div :style="[ colorStyles, { fontSize: `${size}px` }, ]"></div> </template>
HTML
1
2
<div style="color: red; font-size: 16px; background-color: blue;"></div> <div style="color: red; font-size: 16px; background-color: blue;"></div>
Output

# Element refs

When you need to access an element directly, give a name to the element's ref attribute and look up that name with the useTemplateRef() function.
The ref is filled in after the component is mounted, so access it inside the onMounted() hook.

The useTemplateRef() function is available in 3.5+.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> import { useTemplateRef, onMounted } from 'vue' const inputRef = useTemplateRef('my-input') onMounted(() => { inputRef.value?.focus() }) </script> <template> <input ref="my-input" /> </template>

The argument of useTemplateRef() is the string you set in the ref attribute of the element you want to find.
Conversely, if the argument and the value of the ref attribute differ, the element isn't found and the ref becomes null.

You can also reference several elements, like this.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts"> import { ref, useTemplateRef, onMounted } from 'vue' const fruits = ref(['Apple', 'Banana', 'Orange']) const liRefs = useTemplateRef('items') onMounted(() => { liRefs.value?.forEach(liEl => { console.log(liEl.textContent) }) }) </script> <template> <ul> <li v-for="fruit in fruits" ref="items" :key="fruit"> {{ fruit }} </li> </ul> </template>

The order of the ref array is not guaranteed to match the order of the source array.
If you need to find a specific element, identify it by the element's data attributes or another condition instead of the index.

You can also use ref instead of useTemplateRef().
However, you must specify the type of the element to reference with a generic.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup lang="ts"> import { ref, onMounted } from 'vue' const inputRef = ref<HTMLInputElement | null>(null) const fruits = ref(['Apple', 'Banana', 'Orange']) const liRefs = ref<HTMLLIElement[]>([]) onMounted(() => { inputRef.value?.focus() liRefs.value?.forEach(liEl => { console.log(liEl.textContent) }) }) </script> <template> <input ref="inputRef" /> <ul> <li v-for="fruit in fruits" ref="items" :key="fruit"> {{ fruit }} </li> </ul> </template>
Element refs in versions before 3.5

# Conditional rendering

With the v-if, v-else-if, and v-else directives, you can render the template according to data conditions.
These directives must be used on adjacent sibling elements.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup lang="ts"> import { ref } from 'vue' const isShow = ref(true) const list = ref([]) </script> <template> <!-- v-if --> <div v-if="isShow"></div> <div v-if="isShow && list.length"></div> <div v-if="isShow || list.length"></div> <!-- v-else --> <div v-if="isShow"></div> <div v-else></div> <!-- v-else-if --> <div v-if="isShow"></div> <div v-else-if="list.length"></div> <div v-else></div> </template>

The v-if, v-else-if, and v-else directives must be used on a single element, but if you need conditional rendering for multiple elements, use the <template> tag.

VUE
1
2
3
4
5
<script setup lang="ts"> import { ref } from 'vue' const isShow = ref(true) </script>

Adding the condition to every element you render is not recommended.

VUE
1
2
3
4
5
<template> <h1 v-if="isShow"></h1> <div v-if="isShow"></div> <p v-if="isShow"></p> </template>

Adding a wrapping element is simple, but you end up with an unnecessary parent element (<div>).

VUE
1
2
3
4
5
6
7
<template> <div v-if="isShow"> <h1></h1> <div></div> <p></p> </div> </template>

With the <template> tag, you can add a condition to multiple elements at once without an unnecessary parent element.
In Vue, the <template> tag is not included in the final render.

VUE
1
2
3
4
5
<template v-if="isShow"> <h1></h1> <div></div> <p></p> </template>

# List rendering

With the v-for directive, you can render repeatedly to match the number of items in array data.
The :key attribute is what Vue.js uses to tell each repeated item apart, so it must be primitive data such as a unique string or number.
Be careful not to set reference data such as objects or arrays as the :key attribute.

The scope of the 'item variable' is limited to that element.

VUE
1
2
3
4
5
6
7
8
9
<div v-for="item_variable in array_data" :key="unique_primitive"> </div> <div v-for="(item_variable, index) in array_data" :key="unique_primitive"> </div>
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts"> import { ref } from 'vue' const users = ref([ { name: 'Neo', age: 85 }, { name: 'Lewis', age: 22 }, { name: 'Evan', age: 46 } ]) </script> <template> <div v-for="(user, index) in users" :key="user.name" :data-user-name="user.name" :data-user-age="user.age"> {{ index + 1 }}) {{ user.name }}({{ user.age }}) </div> </template>
HTML
1
2
3
<div data-user-name="Neo" data-user-age="85"> 1) Neo(85) </div> <div data-user-name="Lewis" data-user-age="22"> 2) Lewis(22) </div> <div data-user-name="Evan" data-user-age="46"> 3) Evan(46) </div>
Output

# Looping over object data

With the v-for directive, you can also render object data in key-value form repeatedly.
Like the 'item variable' we saw above, you can use any names you want for value, key, and index.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div v-for="value in object_data" :key="unique_primitive"> </div> <div v-for="(value, key) in object_data" :key="unique_primitive"> </div> <div v-for="(value, key, index) in object_data" :key="unique_primitive"> </div>
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup lang="ts"> import { ref } from 'vue' const userInfo = ref({ name: 'Neo', age: 85, emails: ['thesecon@gmail.com', 'neo@zillinks.com'] }) </script> <template> <div v-for="(value, key, index) in userInfo" :key="key"> <template v-if="Array.isArray(value)"> {{ index + 1 }}) {{ key }}: {{ value.join(', ') }} </template> <template v-else> {{ index + 1 }}) {{ key }}: {{ value }} </template> </div> </template>
HTML
1
2
3
<div>1) name: Neo</div> <div>2) age: 85</div> <div>3) emails: thesecon@gmail.com, neo@zillinks.com</div>
Output

# Looping over a number range

In the v-for directive, you can use an integer to output an element repeatedly.
In the following example, the item variable n starts from 1, not 0.

VUE
1
2
3
4
5
6
7
<template> <div v-for="n in 4" :key="n"> {{ n }} </div> </template>
HTML
1
2
3
4
<div>1</div> <div>2</div> <div>3</div> <div>4</div>
Output

# v-if and v-for

If you use the v-if and v-for (list rendering) directives together on an element, v-if is evaluated first.
The two directives conflict functionally when used on a single element, so be careful not to use them together on a single element in the first place!

VUE
1
2
3
4
5
6
<script setup lang="ts"> import { ref } from 'vue' const isShow = ref(true) const list = ref([1, 2, 3]) </script>

Both of the following approaches produce the same output.

VUE
1
2
3
4
5
6
7
8
9
<!-- Not recommended --> <template> <div v-if="isShow" v-for="item in list" :key="item"> {{ item }} </div> </template>
VUE
1
2
3
4
5
6
7
8
<!-- Recommended --> <template v-if="isShow"> <div v-for="item in list" :key="item"> {{ item }} </div> </template>

When you output only some elements repeatedly based on a condition (filtering), you can also use the v-for directive on a <template> tag so that the two directives aren't used on a single element.
In this case, write the key attribute on the <template> tag as well.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts"> import { ref } from 'vue' const list = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) </script> <template> <ul> <template v-for="item in list" :key="item"> <li v-if="item > 3 && item < 8"> {{ item }} </li> </template> </ul> </template>
HTML
1
2
3
4
5
6
<ul> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
Output

# Event handling

You can register event handlers on elements with the v-on:event pattern.
The @event pattern (shorthand) is recommended over the v-on:event pattern because it is shorter and cleaner.
Simple expressions can be written as inline handlers.
Or you can write them by connecting an already defined function (method handlers).

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { ref } from 'vue' const count = ref(0) function handler() { count.value += 1 } </script> <template> <h1>{{ count }}</h1> <button @click="count += 1">Increase!</button> <!-- Inline handler --> <button @click="handler">Increase!</button> <!-- Method handler --> </template>

When you call a method in an inline handler, you can pass the event object ($event) as an argument.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup lang="ts"> import { ref } from 'vue' const urls = ref([ 'heropy.dev', 'github.com', 'google.com', ]) function handler(name: string, event: MouseEvent) { event.preventDefault() location.href = `https://${name}` } </script> <template> <a v-for="url in urls" :key="url" :href="`https://${url}`" @click="handler(url, $event)"> {{ url }} </a> </template>

You can register several handlers for one event by separating them with , or ;.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts"> import { ref } from 'vue' const count = ref(0) function increase() { count.value += 1 } function log(event: MouseEvent) { console.log(event) } </script> <template> <h1>{{ count }}</h1> <button @click="increase(), log($event)">Increase!</button> <button @click="increase(); log($event)">Increase!</button> </template>

# Modifiers

If you use modifiers on an event handler, you can give it extra behavior.
Modifiers can be chained.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template> <a @click.stop="handler">Click!</a> <input @keydown.stop="handler" /> <!-- Using only a modifier without a handler --> <a @click.stop>Click!</a> <input @keydown.stop /> <!-- Chaining modifiers --> <a @click.stop.prevent="handler">Click!</a> <input @keydown.stop.enter="handler" /> <!-- Exact key combination --> <a @click.ctrl.exact="handler">Click!</a> <input @keydown.alt.enter.exact="handler" /> </template>

# Event modifiers

Modifier Description Note
.prevent Prevents the default behavior event.preventDefault()
.stop Stops event propagation event.stopPropagation()
.capture Registers the event handler in capture mode { capture: true }
.once Registers a handler that runs only once { once: true }
.passive Improves perceived performance by separating screen behavior from logic { passive: true }
.self Calls the handler only when the element the event is registered on is the element where it occurred if (target === currentTarget)

# Key modifiers

Modifier Description Note
.KEY-NAME Keyboard key name in kebab-case
.exact Exact key combination
.enter Enter key
.tab Tab key
.delete Backspace and Delete keys
.esc Escape key
.space [Space] key
.up ArrowUp key
.down ArrowDown key
.left ArrowLeft key
.right ArrowRight key
.ctrl Control key
.alt Alt key
.shift Shift key
.meta Meta key Cmd or Win

# Mouse modifiers

Modifier Description Note
.left Left mouse button click click
.right Right mouse button click contextmenu
.middle Mouse wheel button click auxclick

# Form input bindings

On HTML form elements such as <input>, <textarea>, and <select>, you can set up two-way data binding with attribute bindings and event handling.
And with the v-model directive, you can set up two-way data binding even more concisely.

Korean (and other CJK scripts) has more characters than the keyboard has keys, so an IME (Input Method Editor) combines several keystrokes into one character, and this in-progress combining state is called composition.
The v-model directive doesn't update the data while IME composition is in progress, so when you type Korean, the data lags one character behind.
If characters still being composed must be reflected right away, use data binding through attributes and event handling instead of the v-model directive.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { ref } from 'vue' const text = ref('') </script> <template> <!-- Two-way data binding --> <input :value="text" @input="text = ($event.target as HTMLInputElement).value" /> <!-- Two-way data binding with the v-model directive --> <input v-model="text" /> </template>

If you don't use the shorthand v-model directive, each element uses the following attributes and events.

Element Attribute used Event used Note
<input type="text" /> :value @input
<textarea> :value @input
<input type="checkbox" /> :checked @change
<input type="radio" /> :checked @change
<select> :value @change

# Binding patterns by element

# Text

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { ref } from 'vue' const text = ref('') </script> <template> <!-- In general, --> <input v-model="text" /> <!-- When you need Korean (IME) input, --> <input :value="text" @input="text = ($event.target as HTMLInputElement)?.value" /> <h1>{{ text }}</h1> </template>

# Multiline text

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { ref } from 'vue' const text = ref('') </script> <template> <!-- In general, --> <textarea v-model="text"></textarea> <!-- When you need Korean (IME) input, --> <textarea :value="text" @input="text = ($event.target as HTMLTextAreaElement)?.value"></textarea> <pre>{{ text }}</pre> </template>

# Single checkbox

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { ref } from 'vue' const checked = ref(false) </script> <template> <label> <input v-model="checked" type="checkbox" /> I agree. </label> <h2>Agreed: {{ checked ? 'Yes' : 'No' }}</h2> </template>

# Multiple checkboxes

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup lang="ts"> import { ref } from 'vue' interface Fruit { text: string value: string } const fruits = ref<Fruit[]>([ { text: 'Apple', value: 'apple' }, { text: 'Banana', value: 'banana' }, { text: 'Cherry', value: 'cherry' } ]) const checked = ref<string[]>([]) function setChecked(event: Event, fruit: Fruit) { checked.value = (event.target as HTMLInputElement)?.checked ? checked.value.concat([fruit.value]) : checked.value.filter(v => v !== fruit.value) } </script> <template> <label v-for="fruit in fruits" :key="fruit.value"> <input :checked="checked.includes(fruit.value)" :value="fruit.value" type="checkbox" @change="setChecked($event, fruit)" /> {{ fruit.text }} </label> <h2>Selected fruits: {{ checked.join(', ') }}</h2> </template>

# Radio buttons

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup lang="ts"> import { ref } from 'vue' interface Fruit { text: string value: string } const fruits = ref<Fruit[]>([ { text: 'Apple', value: 'apple' }, { text: 'Banana', value: 'banana' }, { text: 'Cherry', value: 'cherry' } ]) const picked = ref('') </script> <template> <label v-for="fruit in fruits" :key="fruit.value"> <input v-model="picked" type="radio" :value="fruit.value" /> {{ fruit.text }} </label> <h2>Selected fruit: {{ picked }}</h2> </template>

# Single select

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script setup lang="ts"> import { ref } from 'vue' interface Fruit { text: string value: string } const fruits = ref<Fruit[]>([ { text: 'Apple', value: 'apple' }, { text: 'Banana', value: 'banana' }, { text: 'Cherry', value: 'cherry' } ]) const selected = ref('') </script> <template> <select v-model="selected"> <option value="" disabled> Choose your favorite fruit. </option> <option v-for="fruit in fruits" :key="fruit.value" :value="fruit.value"> {{ fruit.text }} </option> </select> <h2>Selected fruit: {{ selected }}</h2> </template>

# Multiple select

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup lang="ts"> import { ref } from 'vue' const fruits = ref([ { text: 'Apple', value: 'apple' }, { text: 'Banana', value: 'banana' }, { text: 'Cherry', value: 'cherry' } ]) const selected = ref<string[]>([]) </script> <template> <select v-model="selected" multiple> <option value="" disabled> Choose your favorite fruit. </option> <option v-for="fruit in fruits" :key="fruit.value" :value="fruit.value"> {{ fruit.text }} </option> </select> <h2>Selected fruits: {{ selected }}</h2> </template>

# Editable elements

The following is an example of an element that uses the HTML contenteditable attribute.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { ref, useTemplateRef, watch } from 'vue' const text = ref('') const inputRef = useTemplateRef('input') // Sync the inputRef content with text updates watch(text, newValue => { if (inputRef.value && inputRef.value.innerText !== newValue) { inputRef.value.innerText = newValue } }) </script> <template> <div ref="input" contenteditable @input="text = ($event.target as HTMLDivElement).innerText"></div> <pre>{{ text }}</pre> </template>

# Modifiers

If you use modifiers on the v-model directive of an input element, you can give it extra behavior.

Modifier Description Note
.lazy Updates the data after input is finished (Enter, Tab, etc.) @change
.number Converts to the number data type type="number"
.trim Removes leading and trailing whitespace from the entered text text.trim()
VUE
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts"> import { ref } from 'vue' const text = ref('') const count = ref(0) </script> <template> <input v-model.lazy="text" /> <input v-model.number="count" /> <input v-model.trim="text" /> </template>

# Computed data

With a getter, you can create new computed data based on reactive data.

A getter is a function that is called when data is read (get).
A setter is a function that is called when data is set (set).

Don't use computed data (getters) for asynchronous requests or DOM changes.
If you need them, use a watcher!

TS
1
const computed_data = computed(getter)
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts"> import { ref, computed } from 'vue' const count = ref(0) const double = computed(() => count.value * 2) function increase() { count.value += 1 } </script> <template> <div>{{ count }}</div> <div>{{ double }}</div> <button @click="increase">Increase!</button> </template>

A getter is read-only, so you can add a setter to be called when the data is set.
Instead of a getter, pass an object with get and set properties as the argument of computed().

TS
1
2
3
4
const computed_data = computed({ get: getter, set: setter })
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { ref, computed } from 'vue' const count = ref(0) const double = computed({ get: () => count.value * 2, set: val => { count.value = val / 2 } }) function increase() { double.value += 1 } </script> <template> <div>{{ count }}</div> <div>{{ double }}</div> <button @click="increase">Increase!</button> </template>

# Watching data

Function Description Runs immediately Old value Deep watch Options
watch() Watches explicitly given reactive data X O Both explicit and implicit immediate, deep, once, flush
watchEffect() Watches all reactive data used in the callback O X Explicit only flush

Each option works as follows.

Option Description Default value Note
immediate Calls the callback once when watching starts false
deep Watches deeply by traversing nested properties false
deep: number Sets the maximum depth to traverse 3.5+
once Calls the callback only once and stops watching false 3.4+
flush When the callback is called pre

The flush option can have the following values.

Value Description Default value Alias
pre When state changes, calls the callback before the screen updates O
post When state changes, calls the callback after the screen updates watchPostEffect()
sync Even when state changes several times, calls the callback the same number of times in the same tick, without buffering watchSyncEffect()

# Watch patterns

# Watching a single value

The watch() function lets you check the old value (oldVal), but the watchEffect() function doesn't.
The watchEffect() function runs immediately without any extra option, so the initial value (1) is logged to the console from the start, and when you click the button, the new value (7) is logged.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts"> import { ref, watch, watchEffect } from 'vue' const count = ref(1) watch(count, (newVal, oldVal) => { console.log(newVal, oldVal) // 7, 1 }) watchEffect(() => { console.log(count.value) // 1 // 7 }) </script> <template> <!-- Click the button! --> <button @click="count = 7">Assign a new value!</button> </template>

# Watching multiple values

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { ref, watch, watchEffect } from 'vue' const count = ref(1) const double = ref(2) watch([count, double], ([newCount, newDouble], [oldCount, oldDouble]) => { console.log(newCount, oldCount) // 7, 1 console.log(newDouble, oldDouble) // 14, 2 }) watchEffect(() => { console.log(count.value) // 1 // 7 console.log(double.value) // 2 // 14 }) function assignValue() { count.value = 7 double.value = 14 } </script>

# Deep watching

Reactive data created with the ref() function is not deeply watched implicitly, so when you need it, use the deep option of the watch() function.
Reactive data created with the reactive() function is deeply watched implicitly, so you don't need the deep option when you watch it with the watch() function.

Implicit deep watching traverses every nested property of the target, so it can hurt performance.
In 3.5+, you can limit the maximum depth to traverse by setting a number in the deep option.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script setup lang="ts"> import { ref, watch, watchEffect } from 'vue' const user = ref({ name: 'HEROPY', address: { city: 'Busan' } }) watch(user, newVal => { console.log(newVal) // No reaction! }) watch( user, newVal => { console.log(newVal) // The user object }, { deep: true // Implicit deep watch } ) watch( () => user.value.address.city, // Explicit deep watch newVal => { console.log(newVal) // 'Seoul' } ) watchEffect(() => { console.log(user.value.address.city) // 'Busan' // 'Seoul' }) </script> <template> <!-- Click the button! --> <button @click="user.address.city = 'Seoul'">Assign a new value!</button> </template>

If you set a number in the deep option, it traverses only down to that depth.
Depth is counted from the direct properties of the object that ref() wraps, as level 1.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup lang="ts"> import { ref, watch } from 'vue' const user = ref({ name: 'HEROPY', address: { city: 'Seoul' } }) watch( user, newVal => { console.log(newVal) // Reacts only to name changes! }, { deep: 1 // Traverses down to the properties of user } ) </script> <template> <!-- Click the button! --> <button @click="user.name += '!'">Change name!</button> <button @click="user.address.city += '!'">Change city!</button> <button @click="user.address = { city: 'Jeju' }">Change address!</button> </template>

In the example above, deep: 1 reacts when user.address is replaced as a whole, but doesn't react to changes in user.address.city.

# Stopping a watcher

You can stop watching by calling the value (function) returned by each watch function.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts"> import { ref, watch, watchEffect } from 'vue' const count = ref(0) const unwatch = watch(count, (newVal, oldVal) => { console.log('watch:', newVal, oldVal) }) const unwatchEffect = watchEffect(() => { console.log('watchEffect:', count.value) }) </script> <template> <button @click="count += 1">Assign a new value!</button> <button @click="unwatch">Stop watch()</button> <button @click="unwatchEffect">Stop watchEffect()</button> </template>

# Cleanup callbacks

If the callback starts work such as an asynchronous request or a timer, you need to cancel the previous work when the next watch is triggered.
If you register a cleanup callback with the onWatcherCleanup() function, it is called right before the watcher is invalidated and runs again, and when watching stops.

The onWatcherCleanup() function is available in 3.5+.
Also, you must call it while the callback runs synchronously, not after an await.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { ref, watch, onWatcherCleanup } from 'vue' const userId = ref(1) watch(userId, newVal => { const controller = new AbortController() fetch(`https://jsonplaceholder.typicode.com/users/${newVal}`, { signal: controller.signal }) onWatcherCleanup(() => { controller.abort() // Cancel the previous request! }) }) </script> <template> <button @click="userId += 1">Next user!</button> </template>

# computed and watch

computed() and watch() both react to changes in reactive data, but they have different purposes.
computed() creates a new value from other data, and watch() registers an action to run after the data changes.

Criterion computed() watch()
Purpose Create a value Run an action in response to changes
Returns Computed value (Ref) Function that stops watching
Caching O, recomputes only when its dependencies change X
Watch target Automatically tracks the data used in the getter Given explicitly as the first argument
Async work Not used Can be used

It's easy to decide by asking whether the result is a value to show on the screen.
If you're creating a value to use in the template, use computed(), and if it's something that happens outside the value, such as saving to local storage or requesting data, it's better to use watch().

The following is a common way of creating a derived value with watch().
It works, but the state grows to two values, and the job of keeping the two values from drifting apart falls on you.
If code that changes price appears in several places, missing just one of them makes the value on the screen drift.

VUE
1
2
3
4
5
6
7
8
9
10
<script setup lang="ts"> import { ref, watch } from 'vue' const price = ref(1000) const formattedPrice = ref('') watch(price, newVal => { formattedPrice.value = newVal.toLocaleString() }, { immediate: true }) </script>

With computed(), the state stays as one value, and when price changes, the value follows.
You also don't need the immediate option to fill in the initial value.

VUE
1
2
3
4
5
6
<script setup lang="ts"> import { ref, computed } from 'vue' const price = ref(1000) const formattedPrice = computed(() => price.value.toLocaleString()) </script>

On the other hand, if creating the value requires asynchronous work, use watch().
A getter is called when the value is read, so if you request data inside it, you can't predict when or how many times the request will be sent.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts"> import { ref, watch } from 'vue' const userId = ref(1) const userName = ref('') watch(userId, async newVal => { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${newVal}`) const user = await res.json() userName.value = user.name }) </script>

# Lifecycle hooks

Lifecycle hooks are functions that are called at each stage of a component's lifecycle.
In the Options API, the created and mounted hooks are used most, but the Composition API has no created (beforeCreate) hook.

Hook Description Note
onMounted() After the component is mounted
onUpdated() After the component updates the screen
onUnmounted() After the component is unmounted
onBeforeMount() Before the component is mounted
onBeforeUpdate() Before the component updates the screen
onBeforeUnmount() Before the component is unmounted
onErrorCaptured() After an error is captured
onActivated() After a component cached with <KeepAlive> is shown No SSR
onDeactivated() After a component cached with <KeepAlive> is removed No SSR
onServerPrefetch() Registers an async function to resolve before the component is rendered on the server SSR only
onRenderTracked() After component rendering tracks reactive data Dev only
onRenderTriggered() When a change in reactive data triggers a component re-render Dev only
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> import { useTemplateRef, onMounted } from 'vue' const inputRef = useTemplateRef('input') onMounted(() => { inputRef.value?.focus() }) </script> <template> <input ref="input" /> </template>
Lifecycle hook example

# Composables

You can define functions called composables to encapsulate and reuse Vue logic.
Their names are defined with the use prefix, and you can think of them as a concept similar to React's custom hooks.

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ref } from 'vue' export function useCounter(initialCount = 0) { const count = ref(initialCount) function increase() { count.value += 1 } function decrease() { count.value -= 1 } return { count, increase, decrease } }

Composables are an efficient way to keep independent states that don't interfere with each other.

VUE
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts"> import { useCounter } from '@/composables/useCounter' const counter1 = useCounter() const counter2 = useCounter(7) </script> <template> <div @click="counter1.increase">{{ counter1.count }}</div> <div @click="counter2.decrease">{{ counter2.count }}</div> </template>

Not only the functions you define yourself, but also the functions Vue provides by default follow the same naming convention.
useTemplateRef(), which we saw earlier, is a typical example.

Composable Purpose
useTemplateRef() References an element named with the ref attribute (3.5+)
useId() Generates unique IDs for accessibility attributes or form elements (3.5+)
useAttrs() Gets the fallthrough attributes passed by the parent
useSlots() Gets the slots passed by the parent

# Components

# Registration

If needed, you can register a *.vue component globally as follows and use it anywhere.

Globally registered components prevent tree shaking, so they are included in the final bundle even if you never use them after registering.
So it's best to register components globally only when you really need to, such as when you use them often.

/src/components/TheButton.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> defineProps<{ color: string }>() </script> <template> <button :style="{ backgroundColor: color }" class="btn"> <slot></slot> </button> </template>
/src/main.ts
TS
1
2
3
4
5
6
7
import { createApp } from 'vue' import App from './App.vue' import TheButton from './components/TheButton.vue' createApp(App) .component('TheButton', TheButton) // Register a global component! .mount('#app')
/src/App.vue
VUE
1
2
3
<template> <TheButton color="red">Button!</TheButton> </template>
You can use the component right away without importing it.

Using components locally is recommended because they are bundled efficiently.
In the Composition API, after importing a component in the (parent) component, you can use it in the template right away without registering it separately.

/src/App.vue
VUE
1
2
3
4
5
6
7
<script setup lang="ts"> import TheButton from './components/TheButton.vue' </script> <template> <TheButton color="red">Button!</TheButton> </template>

# Props

To pass data from a parent component to a child component, you use props.
defineProps() is a compiler macro, so you can use it right away without importing it.

In the script, you can access each prop through the props object returned by defineProps().
In the template, you can access each prop directly without the props object.
So the names of your reactive data must not overlap with the prop names.

In the template, you can also access props through the $props object, but this is not recommended because using just the names is more concise.
Objects with the $ prefix, like $props, are built-in properties of the component.

TS
1
2
3
4
interface Props { // Prop types... } const props = defineProps<Props>()

If a prop needs a default value, destructure the return value of defineProps() and use JavaScript's default value syntax as is.
This is called Reactive Props Destructure, and the destructured variables stay reactive as well.

/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts"> const { myValue, backgroundColor = 'yellow' } = defineProps<{ myValue: string backgroundColor?: string // Optional prop }>() console.log(myValue, backgroundColor) </script> <template> <input :style="{ backgroundColor }" :value="myValue" /> </template>

Reactive Props Destructure is enabled by default in 3.5+.
In earlier versions, destructured variables lose their reactivity, so you must set default values with the withDefaults() macro.

withDefaults() must wrap the defineProps() call directly.
And default values of reference types, such as arrays or objects, must be wrapped in a function that returns them, so that each component instance gets its own value.

/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> const props = withDefaults( defineProps<{ myValue: string backgroundColor?: string tags?: string[] }>(), { backgroundColor: 'yellow', tags: () => [] // Wrap reference-type defaults in a function! } ) console.log(props) </script> <template> <input :style="{ backgroundColor }" :value="myValue" /> </template>
Prop default values in versions before 3.5

When you pass props from the parent component, the convention is to use kebab-case to match HTML attribute notation.
camelCase also works in SFC templates, but only kebab-case works in in-DOM templates.

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts"> import { ref } from 'vue' import TextField from './components/TextField.vue' const text = ref('Hello World!') </script> <template> <TextField :my-value="text" background-color="royalblue" /> </template>

# Compiler macros

A compiler macro is not a function that runs; it is a declaration that is turned into other code and disappears at the compile step.

VUE
1
2
3
<script setup lang="ts"> const props = defineProps<{ title: string }>() </script>
Before compiling
JS
1
2
3
4
5
6
7
8
9
// // The defineProps() call doesn't remain. (Some details omitted) export default { props: { title: { type: String, required: true } }, setup(__props) { const props = __props } }
After compiling

defineEmits(), withDefaults(), defineModel(), and defineOptions() are macros too.

Macros are recognized only at the top level of <script setup>.
They are hoisted out of setup(), so they can't reference local variables.
And if you call them inside a conditional or a function, they aren't transformed and stay as they are, which causes a runtime error.

# Emits

A child component can read the data that the parent component passes through props, but it can't change it.
If it needs to be changed, the child component can use emits to fire a (custom) event to the parent component and pass data along with it.
Or it can fire only an event without any data.

defineEmits() is also a compiler macro, so you can use it right away without importing it.
Define the names of the events to emit, and pass the data to attach when you emit the event.

Use the emit function returned by defineEmits(), or the $emit function in the template.

TS
1
2
3
const emit = defineEmits(['customEvent', ...]) emit('customEvent', data)
/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> const { myValue, backgroundColor = 'yellow' } = defineProps<{ myValue: string backgroundColor?: string }>() const emit = defineEmits(['update', 'search']) console.log(myValue, backgroundColor, emit) function handler(event: Event) { emit('update', (event.target as HTMLInputElement)?.value) } </script> <template> <input :style="{ backgroundColor }" :value="myValue" @input="handler" /> <button @click="$emit('search', myValue)">Search</button> </template>

The parent component can listen for events emitted this way with the v-on (@) directive.
And the attached data is passed as the first argument of the handler ($event in the template).

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts"> import { ref } from 'vue' import TextField from './components/TextField.vue' const text = ref('') function search(keyword: string) { console.log(keyword) // The entered value } </script> <template> <TextField :my-value="text" @update="text = $event" @search="search" /> </template>

# Slots

Everything you write between the opening and closing tags of a component, like an HTML element, is passed to the child component's slot.

VUE
1
2
3
4
5
6
7
<template> <MyComponent> <h2></h2> <div></div> <p></p> </MyComponent> </template>

# Default slots and fallback content

A <slot> element without a name attribute (without a name) is called the default slot or slot outlet.
And you can write fallback content, which is shown when nothing is passed to the component's slot, between the opening and closing <slot> tags.

/src/components/TheButton.vue
VUE
1
2
3
4
5
<template> <button class="btn"> <slot>No button name...</slot> </button> </template>
/src/App.vue
VUE
1
2
3
4
5
6
7
8
<script setup lang="ts"> import TheButton from './components/TheButton.vue' </script> <template> <TheButton /> <TheButton>Click me!</TheButton> </template>
HTML
1
2
<button class="btn">No button name...</button> <button class="btn">Click me!</button>
Output

# Named slots

You can name a <slot> element by adding a name attribute, and these are called named slots.
The name of a <slot> element without a name attribute is default, and you can omit it.

You can match the content for each slot by name with #slotName.

/src/components/TheButton.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<template> <button class="btn"> <slot name="abc"></slot> <slot></slot> <slot name="xyz"></slot> </button> </template> <style scoped> span { display: block; } </style>
/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts"> import TheButton from './components/TheButton.vue' </script> <template> <TheButton> <template #abc>ABC</template> <template #default>DEFAULT</template> <template #xyz>XYZ</template> </TheButton> </template>

Or you can omit the name of the default slot, like this.

/src/App.vue
VUE
1
2
3
4
5
6
7
<template> <TheButton> <template #abc>ABC</template> DEFAULT <template #xyz>XYZ</template> </TheButton> </template>
HTML
1
2
3
4
5
<button class="btn"> ABC DEFAULT XYZ </button>
Output

# Scoped slots

Scoped slots provide a pattern for passing data through the <slot> element.
Through the child component's slot, you can pass (reactive) data to the parent component.
However, the scope of that data is limited to the slot content.

/src/components/TheMenu.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script setup lang="ts"> import { ref } from 'vue' const isShow = ref(false) function showMenu() { isShow.value = true window.addEventListener('click', hideMenu) } function hideMenu() { isShow.value = false window.removeEventListener('click', hideMenu) } function toggleMenu(event: MouseEvent) { event.stopPropagation() isShow.value ? hideMenu() : showMenu() } </script> <template> <slot name="activator" :status="isShow" :attrs="{ onClick: toggleMenu }"></slot> <div v-if="isShow" class="the-menu" @click.stop> <slot></slot> </div> </template>
/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts"> import TheMenu from './components/TheMenu.vue' </script> <template> <TheMenu> <template #activator="{ attrs, status }"> <button v-bind="attrs"> Menu! ({{ status }}) </button> </template> <!-- You can't use the attrs and status variables here! --> <ul> <li>Apple</li> <li>Banana</li> <li>Cherry</li> </ul> </TheMenu> </template>
HTML
1
2
<button>Menu! (false)</button> <!-- v-if -->
Output

# Fallthrough attributes

Attributes defined as props are not output unless you bind them to an element yourself.
On the other hand, attributes that you write when using a component but didn't define as props are called fallthrough attributes.

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
<script setup lang="ts"> import MyComponent from './components/MyComponent.vue' </script> <template> <MyComponent c="C" d="D" /> </template>
The parent component passes strings to the child component through the c and d attributes

All fallthrough attributes are automatically bound to (inherited by) the root element of the component.

/src/components/MyComponent.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> // Attributes a and b, defined as props but not used defineProps<{ a?: string b?: number }>() </script> <template> <div> <span>Hello!</span> </div> </template>
A component with a single root element
HTML
1
2
3
<div c="C" d="D"> <span>Hello!</span> </div>
Output: the c and d attributes are automatically bound to the single root element

On the other hand, when there are two or more root elements, fallthrough attributes aren't bound to any element.

/src/components/MyComponent.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts"> defineProps<{ a?: string b?: number }>() </script> <template> <div> <span>Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p> </template>
A component with multiple root elements
HTML
1
2
3
4
5
<div> <span>Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p>
Output: the c and d attributes aren't bound to any element

When there are two or more root elements, to pass the fallthrough attributes to the element you want, bind the built-in $attrs object with the v-bind attribute.

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts"> defineProps<{ a?: string b?: number }>() </script> <template> <div> <span v-bind="$attrs">Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p> </template>
HTML
1
2
3
4
5
<div> <span c="C" d="D">Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p>
Output: the c and d attributes are bound to the element you want

If needed, you can keep fallthrough attributes from being bound automatically even when there is only one root element.

/src/components/MyComponent.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> defineProps<{ a?: string b?: number }>() defineOptions({ inheritAttrs: false // Turn OFF the inheritance of fallthrough attributes }) </script> <template> <div> <span>Hello!</span> </div> </template>

You can also access fallthrough attributes in the script.
And the attrs ($attrs) object includes events such as onClick. (It doesn't include props.)

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts"> import MyComponent from './components/MyComponent.vue' function handler() { console.log('Clicked!!') } </script> <template> <MyComponent a="A" :b="2" c="C" d="D" @click="handler" /> </template>
/src/components/MyComponent.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts"> import { useAttrs } from 'vue' defineProps<{ a?: string b?: number }>() const attrs = useAttrs() console.log(attrs) // Log it! </script> <template> <div> <span>Hello!</span> </div> </template>
JS
1
2
3
4
5
{ c: "C", d: "D", onClick: f handler() }
Output

# Two-way data binding (v-model)

When reactive data defined in a parent component is passed as props, the child component can only read it.
To modify the reactive data, you need a pattern where the child uses emits to fire a (custom) event and pass data to the parent component, which has write access.
The props and emits in this pattern can have any names.

/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts"> defineProps<{ abc: string label?: string }>() defineEmits(['xyz']) </script> <template> <label> {{ label }} <input v-bind="$attrs" :value="abc" @input="$emit('xyz', ($event.target as HTMLInputElement)?.value)" /> </label> </template> <style scoped> label { display: block; } </style>
Firing the 'xyz' custom event to modify the 'abc' prop
/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup lang="ts"> import { ref } from 'vue' import TextField from './components/TextField.vue' const email = ref('') const password = ref('') function signIn() { console.log('Sign-in info:', email.value, password.value) } </script> <template> <form @submit.prevent="signIn"> <TextField :abc="email" label="Email" autocomplete="username" @xyz="email = $event" /> <TextField :abc="password" label="Password" type="password" autocomplete="current-password" @xyz="password = $event" /> <button type="submit">Sign in</button> </form> </template>

If you name the prop and emit modelValue and update:modelValue (the names Vue expects), you can use the shorthand v-model directive.

/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts"> defineProps<{ modelValue: string label?: string }>() defineEmits(['update:modelValue']) </script> <template> <label> {{ label }} <input v-bind="$attrs" :value="modelValue" @input="$emit('update:modelValue', ($event.target as HTMLInputElement)?.value)" /> </label> </template> <style scoped> label { display: block; } </style>
/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts"> // ... </script> <template> <form @submit.prevent="signIn"> <TextField v-model="email" label="Email" autocomplete="username" /> <TextField v-model="password" label="Password" type="password" autocomplete="current-password" /> <button type="submit">Sign in</button> </form> </template>

However, declaring a prop and an emit as a pair in the child component every time is tedious.
With the defineModel() macro, you can replace the two declarations with one.
If you use the ref object returned by defineModel() as is, just reading and writing the value updates the parent component's data.

The defineModel() macro is available in 3.4+ and is the recommended way to implement v-model on components.

/src/components/TextField.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> const model = defineModel<string>() defineProps<{ label?: string }>() </script> <template> <label> {{ label }} <input v-bind="$attrs" v-model="model" /> </label> </template> <style scoped> label { display: block; } </style>

The parent component uses it the same way as in the previous v-model example.

If one component needs several pieces of two-way data, give defineModel() a name and connect it with the v-model:name pattern.

/src/components/UserName.vue
VUE
1
2
3
4
5
6
7
8
9
<script setup lang="ts"> const firstName = defineModel<string>('firstName') const lastName = defineModel<string>('lastName') </script> <template> <input v-model="firstName" /> <input v-model="lastName" /> </template>
/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> import { ref } from 'vue' import UserName from './components/UserName.vue' const first = ref('') const last = ref('') </script> <template> <UserName v-model:first-name="first" v-model:last-name="last" /> </template>

# Dynamic components

With the special element in the form of <component :is="component" />, you can render components dynamically.

<component>, <slot>, and <template> are built-in elements with special features used in Vue.

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup lang="ts"> import { ref } from 'vue' import type { Component } from 'vue' import Home from './components/Home.vue' import About from './components/About.vue' import Posts from './components/Posts.vue' interface Tabs { [key: string]: Component // Index signature! } const currentTab = ref('Home') const tabs: Tabs = { Home, About, Posts } function changeTab(tab: string | number) { if (typeof tab === 'string') { currentTab.value = tab } } </script> <template> <button v-for="(_, tab) in tabs" :key="tab" @click="changeTab(tab)"> {{ tab }} </button> <component :is="tabs[currentTab]" /> </template>
A component that changes (renders) dynamically each time you click a button

# Provide / Inject

As we saw earlier, you use props to pass data from a parent to a child component.
But if you want to pass data from an ancestor to a descendant component in one go, beyond a direct parent-child relationship, you can use provide / inject instead of props, which must pass data through every parent-child level.

TS
1
2
3
4
5
// Ancestor component provide(name, data) // Descendant component const data = inject(name)

The following example passes data directly to a descendant component without going through the child component.
Note that to keep reactivity, you must pass the reactive data itself, like count, not just its value, like count.value.

All reactive data, such as ref(), reactive(), and computed(), can keep its reactivity.

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts"> import { ref, provide } from 'vue' import Parent from './components/Parent.vue' const count = ref(0) provide('count', count) </script> <template> <button @click="count += 1"> Increase! </button> <Parent /> </template>
/src/components/Parent.vue
VUE
1
2
3
4
5
6
7
<script setup lang="ts"> import Child from './Child.vue' </script> <template> <Child /> </template>
/src/components/Child.vue
VUE
1
2
3
4
5
6
7
8
9
<script setup lang="ts"> import { inject } from 'vue' const count = inject('count') </script> <template> <h2>{{ count }}</h2> </template>

Data injected with the inject() function is of type unknown by default.
Of course, you can set the type with a generic, but there is no guarantee that the value will be provided at runtime, so the injected value can still be undefined.

/src/components/Child.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> import { inject } from 'vue' import type { Ref } from 'vue' const count = inject<Ref<number>>('count') // const count: Ref<number> | undefined console.log(count.value) // Error! - 'count' is possibly 'undefined'.ts(18048) </script> <template> <h2>{{ count }}</h2> </template>

So you can add a type guard,
or if you are sure the value is always provided, you can use a type assertion.

/src/components/Child.vue
TS
1
2
3
4
5
const count = inject<Ref<number>>('count') if (count && typeof count.value === 'number') { console.log(count.value) }
With a type guard
/src/components/Child.vue
TS
1
2
3
const count = inject('count') as Ref<number> console.log(count.value)
With a type assertion

If you pass the data to provide with the provide() function as an object, you can pass several pieces of data at once.
You can also provide a function that changes the reactive data along with it, so that descendant elements can use it, like this.

/src/App.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup lang="ts"> import { ref, provide } from 'vue' import Parent from './components/Parent.vue' const count = ref(0) provide('App.vue', { count, increase }) // Typing the provided data! export interface Provide { count: typeof count increase: typeof increase } function increase() { count.value += 1 } </script> <template> <button @click="increase">Increase!(App)</button> <Parent /> </template>
/src/components/Child.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts"> import { inject } from 'vue' import type { Provide } from '../App.vue' const { count, increase } = inject('App.vue') as Provide </script> <template> <button @click="increase">Increase!(Child)</button> <h2>{{ count }}</h2> </template>
The 'Increase!' button in the Child component works well.