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.
# 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
12<div id="app"></div> <script type="module" src="/src/main.ts"></script>
123456789import { 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,
1234createApp(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.
1234567891011<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.
123456789<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.
123456<style> div[data-v-7a7a37b1] { color: red; } </style> <div data-v-7a7a37b1></div>
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.
12345678910111213141516<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>
1234567<template> <div class="child-wrap"> <div class="child">CHILD</div> </div> </template> <style scoped></style>
1234567<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>
You can use several <style> tags in one component (SFC).
123<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.
123456789101112131415161718192021<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().
12345678910<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.
12345<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)'.
1234567891011<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>
12<div>123</div> <div>123</div>
# Rendering HTML
To actually render HTML code contained in a string, you need to use the v-html directive instead of double curly braces.
123<script setup lang="ts"> const rawHtml = '<div style="color: red;">Rendered HTML</div>' </script>
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.
1234<template> <div>{{ rawHtml }}</div> <div v-html="rawHtml"></div> </template>
12<div><div style="color: red;">Rendered HTML</div></div> <div style="color: red;">Rendered HTML</div>
# 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.
123<element v-bind:attribute="data" /> <element :attribute="data" /> <element v-bind="attribute_object" />
123456789101112131415161718192021<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>
12345<div id="hello-world"></div> <input disabled /> <button id="hello-world" disabled>Button!</button>
With the :['attributeName'] pattern, you can decide the attribute name dynamically.
1234567891011121314151617181920<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>
# 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() {}.
123456789101112131415161718<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.
12345678910111213141516171819202122232425262728293031323334353637383940<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>
123<div class="btn active btn-primary"></div> <div class="btn active btn-primary"></div> <div class="btn active btn-primary"></div>
# 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.
1234567891011121314151617181920212223242526272829<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>
12<div style="color: red; font-size: 16px; background-color: blue;"></div> <div style="color: red; font-size: 16px; background-color: blue;"></div>
# 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.
12345678910111213<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.
1234567891011121314151617181920212223<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>
You can also use ref instead of useTemplateRef().
However, you must specify the type of the element to reference with a generic.
1234567891011121314151617181920212223242526<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>
# 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.
12345678910111213141516171819202122<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.
12345<script setup lang="ts"> import { ref } from 'vue' const isShow = ref(true) </script>
Adding the condition to every element you render is not recommended.
12345<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>).
1234567<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.
12345<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.
123456789<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>
12345678910111213141516171819<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>
123<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>
# 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.
1234567891011121314<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>
12345678910111213141516171819202122<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>
123<div>1) name: Neo</div> <div>2) age: 85</div> <div>3) emails: thesecon@gmail.com, neo@zillinks.com</div>
# 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.
1234567<template> <div v-for="n in 4" :key="n"> {{ n }} </div> </template>
1234<div>1</div> <div>2</div> <div>3</div> <div>4</div>
# 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!
123456<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.
123456789<!-- Not recommended --> <template> <div v-if="isShow" v-for="item in list" :key="item"> {{ item }} </div> </template>
12345678<!-- 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.
1234567891011121314151617<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>
123456<ul> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
# 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).
123456789101112131415<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.
123456789101112131415161718192021222324<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 ;.
123456789101112131415161718<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.
12345678910111213141516<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.
123456789101112131415<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
123456789101112131415<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
123456789101112131415<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
123456789101112131415<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
12345678910111213141516171819202122232425262728293031323334<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
123456789101112131415161718192021222324252627<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
12345678910111213141516171819202122232425262728293031<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
1234567891011121314151617181920212223242526272829<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.
123456789101112131415161718192021<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() |
123456789101112<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.
1const computed_data = computed(getter)
12345678910111213141516<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().
1234const computed_data = computed({ get: getter, set: setter })
123456789101112131415161718192021<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.
123456789101112131415161718<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
123456789101112131415161718192021<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.
12345678910111213141516171819202122232425262728293031323334353637383940<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.
123456789101112131415161718192021222324252627<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.
12345678910111213141516171819<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.
123456789101112131415161718192021<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.
12345678910<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.
123456<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.
123456789101112<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 |
12345678910111213<script setup lang="ts"> import { useTemplateRef, onMounted } from 'vue' const inputRef = useTemplateRef('input') onMounted(() => { inputRef.value?.focus() }) </script> <template> <input ref="input" /> </template>
# 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.
123456789101112131415161718import { 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.
1234567891011<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.
12345678910111213<script setup lang="ts"> defineProps<{ color: string }>() </script> <template> <button :style="{ backgroundColor: color }" class="btn"> <slot></slot> </button> </template>
1234567import { 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')
123<template> <TheButton color="red">Button!</TheButton> </template>
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.
1234567<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.
1234interface 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.
1234567891011121314<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>
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.
123456789101112131415161718192021<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>
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.
123456789101112<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.
123<script setup lang="ts"> const props = defineProps<{ title: string }>() </script>
123456789// // The defineProps() call doesn't remain. (Some details omitted) export default { props: { title: { type: String, required: true } }, setup(__props) { const props = __props } }
defineEmits(), withDefaults(), defineModel(), and defineOptions() are macros too.
# 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.
123const emit = defineEmits(['customEvent', ...]) emit('customEvent', data)
123456789101112131415161718192021<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).
1234567891011121314151617<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.
1234567<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.
12345<template> <button class="btn"> <slot>No button name...</slot> </button> </template>
12345678<script setup lang="ts"> import TheButton from './components/TheButton.vue' </script> <template> <TheButton /> <TheButton>Click me!</TheButton> </template>
12<button class="btn">No button name...</button> <button class="btn">Click me!</button>
# 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.
12345678910111213<template> <button class="btn"> <slot name="abc"></slot> <slot></slot> <slot name="xyz"></slot> </button> </template> <style scoped> span { display: block; } </style>
1234567891011<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.
1234567<template> <TheButton> <template #abc>ABC</template> DEFAULT <template #xyz>XYZ</template> </TheButton> </template>
12345<button class="btn"> ABC DEFAULT XYZ </button>
# 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.
12345678910111213141516171819202122232425262728293031<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>
12345678910111213141516171819<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>
12<button>Menu! (false)</button> <!-- v-if -->
# 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.
123456789<script setup lang="ts"> import MyComponent from './components/MyComponent.vue' </script> <template> <MyComponent c="C" d="D" /> </template>
All fallthrough attributes are automatically bound to (inherited by) the root element of the component.
12345678910111213<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>
123<div c="C" d="D"> <span>Hello!</span> </div>
On the other hand, when there are two or more root elements, fallthrough attributes aren't bound to any element.
1234567891011121314<script setup lang="ts"> defineProps<{ a?: string b?: number }>() </script> <template> <div> <span>Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p> </template>
12345<div> <span>Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p>
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.
1234567891011121314<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>
12345<div> <span c="C" d="D">Hello!</span> </div> <h2>Hi?</h2> <p>Greetings~</p>
If needed, you can keep fallthrough attributes from being bound automatically even when there is only one root element.
123456789101112131415<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.)
12345678910111213141516<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>
1234567891011121314151617<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>
12345{ c: "C", d: "D", onClick: f handler() }
# 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.
1234567891011121314151617181920212223<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>
12345678910111213141516171819202122232425262728<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.
1234567891011121314151617181920212223<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>
123456789101112131415161718<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.
123456789101112131415161718192021<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.
123456789<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>
12345678910111213<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.
123456789101112131415161718192021222324252627282930313233<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>
# 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.
12345// 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.
1234567891011121314<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>
1234567<script setup lang="ts"> import Child from './Child.vue' </script> <template> <Child /> </template>
123456789<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.
12345678910111213<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.
12345const count = inject<Ref<number>>('count') if (count && typeof count.value === 'number') { console.log(count.value) }
123const count = inject('count') as Ref<number> console.log(count.value)
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.
12345678910111213141516171819202122232425<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>
1234567891011<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>
Thanks for reading to the end.
Likes and kind comments help keep this blog going!