Vue 3
Composition API
<script setup lang="ts">
// code goes here!
</script>
Why use composition?
The Composition API is recommended over mixins.
- solves the drawbacks of mixins:
- unclear which mixin a property comes from
- namespace collisions are possible
- if multiple mixins need to interact they need to share state
- with composables, everything is passed via arguments
- logical concerns can be grouped together
- easier to extract logic into another component or utility
- better type inference
- more efficient compilation
Reactive State & Refs
Note
Use ref
for state that will be replaced wholesale, and reactive
for object types that hold many individual state items.
ref
creates reactive state, by wrapping the argument in an object with avalue
propertyref
values are deeply reactive - if you don't need deep reactivity you can useshallowRef
import { ref } from 'vue'
const count = ref(0)
console.log(count.value)
// deep reactivity
const objRef = ref({ count: 0 })
objRef.value.count++
ref
s at the top level are unwrapped when accessed in the template
const count = ref(0)
const nested = { count: ref(1) }
const { count: nestedCount } = nested
<template>
<h1>{{count}}</h1>
<!-- this doesn't work since nested.count isn't top level -->
<h2>{{nested}}</h2>
<!-- but this does since the destructured property is top level -->
<h3>{{nestedCount}}</h3>
</template>
- you can use
reactive
to directly proxy object types (including arrays)- newly added properties will be reactive (no need for
Vue.set
)
- newly added properties will be reactive (no need for
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
- anything that doesn't preserve the reference to the
reactive
object breaks the reactivity connection, including:- reassigning the whole object
- destructuring
- passing individual properties to a function
reactive
is used to make objects passed toref
deeply reactive, so the same caveats apply
/* do not do any of these! */
// code that uses the first reference will no longer track changes
let state = reactive({ count: 0 })
state = reactive({ count: 1 })
// count will not be reactive
const { count } = reactive({ count: 0 })
// the function will receive a plain number that won't react to changes
callFunction(state.count)
- if a
ref
is a property of areactive
object, it automatically gets unwrapped inside JavaScript, but the same isn't true ofreactive
arrays or collection types likeMap
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // don't need .value
const array = reactive([ref('Hello')])
console.log(array[0].value)
Computed Properties
computed
returns aref
import { reactive, computed } from 'vue'
const person = reactive({ firstName: 'John', lastName: 'Smith' })
const fullName = computed(() => {
return person.firstName + ' ' + person.lastName
})
console.log(fullName.value)
- to provide a setter:
const fullName = computed({
get() {
...
},
set(newValue) {
...
}
})
Lifecycle Hooks
- same names as Vue 2, but begin with
on
- exceptions:
beforeDestroy
->onBeforeUnmount
,destroyed
->onUnmounted
- exceptions:
- must be registered synchronously
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => { ... })
// this won't work
setTimeout(() => {
onMounted(() => { ... })
}, 100)
Watchers
- you can't
watch
a property of a reactive object (because the reference is not reactive), instead use a getter
import { reactive, watch } from 'vue'
const obj = reactive({ count: 0 })
// this won't work
watch(obj.count, (newCount, oldCount) => { ... })
// instead do this
watch(() => obj.count, (newCount, oldCount) => { ... })
- you can watch multiple sources with an array
const x = ref(0)
const y = ref(0)
watch([x, y], ([newX, newY]) => { ... })
- watchers directly on reactive objects are deep, but watchers that use getters are not - to rectify this use the
deep
option
watch(
() => state.someObject,
(newValue, oldValue) => {
// if deep is not used, won't trigger unless state.someObject is replaced
},
{ deep: true }
)
- create eager/immediate watchers using
watchEffect
- automatically tracks dependencies just like computed properties
- only tracks dependencies during synchronous execution - if used with an
async
callback, only dependences accessed before the firstawait
are tracked
- only tracks dependencies during synchronous execution - if used with an
- automatically tracks dependencies just like computed properties
import { watchEffect } from 'vue'
const url = ref('https://...')
const data = ref(null)
watchEffect(async () => {
// will run on mount and when url changes
const response = await fetch(url.value)
data.value = await response.json()
})
- watchers are called before the DOM updates - if you need to access the updated DOM inside a watcher, use the
flush: post
option orwatchPostEffect()
- to stop a watcher, use the function returned from
watch
orwatchEffect
const unwatch = watchEffect(() => { ... })
unwatch() // later
- watchers can be created in async callbacks, but won't be stopped automatically when the owner unmounts, so must be stopped manually
- prefer making the watch logic conditional instead
const data = ref(null)
getAsyncData.then(result => {
data.value = result
// don't do this
watch(data, (newData) => { ... })
})
// instead do this
watch(data, (newData) => {
if (newData.value) {
...
}
})
Template Refs
- declare a template
ref
with the same name as theref
attribute (don't need v-bind)ref
s arenull
on first render
<input ref="input">
import { ref } from 'vue'
const input = ref(null)
console.log(input.value)
- when using
ref
withv-for
, pass in an Array which will be populated with the references after mount- the order is not guaranteed to be the same as the source array order
<li v-for="item in list" ref="itemRefs">{{ item }}</li>
const list = ref([ ... ])
const itemRefs = ref([])
ref
can also be bound to a function that will run on each component update, and receives the reference as the first argument
<input :ref="(el) => { /* assign el to a property or ref */ }">
- referenced components using
<script setup>
are private by default, so theref
cannot access anything unless the component exposes it using thedefineExpose
macro- macros do not need to be
import
ed
- macros do not need to be
/* child component */
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({ a, b })
/* parent component */
// child has the shape { a: number, b: number }
const child = ref(null)
Props
- define props using the
defineProps
macro, which returns an object with all the passed prop values- or use type-based declaration
- code inside
defineProps
can't access other variables declared in<script setup>
const props = defineProps({
author: String,
year: [String, Number], // can be either
title: {
type: String,
required: true
},
type: {
type: String,
default: 'Nonfiction',
validator(value) {
return ['Fiction', 'Nonfiction'].includes(value)
}
},
genres: {
type: Array,
default(rawProps) {
// object or array defaults must be returned from a factory function, which receives the raw props as an argument
return ['adventure', 'sci-fi']
}
}
})
console.log(props.title)
Events (emits) and v-model
- to emit events in inline handlers, use
$emit
- events should be documented using the
defineEmits
macro, which returns anemit
function that can be used in the script- or use type-based declaration
const emit = defineEmits({
text: String,
click: null, // no validation
submit({ email, password }) => {
return email && password
}
})
emit('submit', { email, password })
- to set up a
v-model
on a component, use a prop namedmodelValue
and anupdate:modelValue
event- to change the name of the prop, pass an argument to the
v-model
directive - this lets you have multiplev-model
s on one component- this replaces the
.sync
modifier onv-bind
- this replaces the
- to change the name of the prop, pass an argument to the
<ChildComponent v-model:title="pageTitle" />
<!-- is shorthand for: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
v-model
can support custom modifiers
Fallthrough Attributes
- to forward non-prop attributes (including
class
andstyle
) and listeners to an element that isn't the root, setinheritAttrs: false
and use$attrs
- components with multiple root nodes have
inheritAttrs
off by default
- components with multiple root nodes have
<div class="btn-wrapper">
<button v-bind="$attrs">Click me</button>
</div>
<script>
// use a normal <script> block to declare options
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup logic here
</script>
- fallthrough attributes can be accessed in the script using
useAttrs()
(note that this isn't reactive)
Provide and Inject
provide
key can be a string or Symbol- if you are working in a large application, use Symbol keys to avoid collisions, and export/import the keys from a dedicated file
- Symbol keys also work better in TypeScript
provide
value can be anything, including reactive state- injected
ref
s are not automatically unwrapped
import { provide } from 'vue'
provide('message', 'hello!')
// in another component
import { inject } from 'vue'
const message = inject('message')
- specify a default value for
inject
- if the default value is expensive, create it in a factory function
const value = inject('message', 'default value')
const value2 = inject('key', () => new ExpensiveClass())
- keep any state mutations inside the provider if possible
- if you need to update the provided state from an injector component,
provide
a function to mutate it
// provider component
const location = ref('North Pole')
function updateLocation(newLocation) {
location.value = newLocation
}
provide('location', { location, updateLocation })
// injector component
const { location, updateLocation } = inject('location')
- wrap provided value with
readonly
to ensure injector components can't mutate it
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
Composables
- always start with
use
(like React hooks) - should only be called synchronously
- composables can call other composables, to break down logic into small, isolated units
- instead of returning a
reactive
, return a plain object containing multipleref
s, so that it can be destructured - make sure to clean up side effects using
onUnmounted
import { ref, onMounted, onUnmounted } from 'vue'
import { useEventListener } from './eventListener.js'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// see below
useEventListener(window, 'mousemove', update)
// state must be returned
return { x, y }
}
// in a component:
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
// or
const mouse = reactive(useMouse())
console.log(mouse.x)
- example event listener component:
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
- composables can accept arguments, including
ref
s, and usewatchEffect
to re-run code when theref
changes
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
data.value = null
error.value = null
// unref() unwraps potential refs
fetch(unref(url))
.then(res => res.json())
.then(json => (data.value = json))
.catch(err => (error.value = err))
}
if (isRef(url)) {
// set up reactive re-fetch if input URL is a ref
watchEffect(doFetch)
} else {
doFetch()
}
return { data, error }
}
Directives
- objects that start with
v
and have specific hooks
<script setup>
const vFocus = {
mounted: el => el.focus()
//// other hooks:
// created
// beforeMount
// beforeUpdate
// updated
// beforeUnmount
// unmounted
}
</script>
<template>
<input v-focus />
</template>
- directive hooks get the following arguments:
el
: the DOM elementbinding
: an object with these properties:value
: always treated as an expression (ex. ifv-directive="1+1"
thenvalue === 2
)- can be an object
oldValue
: the previous value, only inbeforeUpdate
andupdated
arg
: ex.v-directive:arg
modifiers
: ex.v-directive.foo.bar
->{ foo: true, bar: true }
instance
: the component instancedir
: the directive object
vnode
: the VNode of the bound elementprevNode
: the VNode of the previous render, only inbeforeUpdate
andupdated
- hook arguments (except for
el
) are read-only - to share information across hooks use the element'sdataset
- directive arguments can be dynamic
<div v-example:[arg]="value"></div>
- directives that have the same behavior for
mounted
andupdated
, and no other hooks, can be declared as a function
const vColor = (el, binding) => {
el.style.color = binding.value
}
- it is recommended not to use custom directives on components - directives always apply to a component's root node and can't be forwarded (unlike #Fallthrough Attributes)
Plugins
import { createApp } from 'vue'
const app = createApp({})
const myPlugin = {
install(app, options) {
// global methods
app.config.globalProperties.$translate = key => { ... }
// global `provide` values
app.provide('message', 'hello!')
// global components
app.component('my-component', /* imported component */ )
// global directives
app.directive('my-directive', /* directive object or function */ })
}
}
app.use(myPlugin, {
/* plugin options go here */
})
TypeScript typing
Refs and computed
ref
s andcomputed
properties have inferred types, but can be typed explicitly by passing a generic argument to the creation function- omit the initial value to create an optional ref
const year = ref<string | number>('2020')
const double = computed<number>(() => { ... })
- template
ref
s should be explicitly typed
<script setup lang="ts">
const el = ref<HTMLInputElement | null>(null)
</script>
<template>
<input ref="el" />
</template>
- type component
ref
s like this
const modal = ref<InstanceType<typeof MyModal> | null>(null)
Reactive
reactive
objects also have inferred types, or can be typed using interfaces- don't use the generic argument for
reactive
- don't use the generic argument for
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 Guide' })
Props
-
prop types can be inferred from
defineProps
, or using a generic argument (type-based declaration)- if using type-based declaration:
- generic argument must be either an inline object literal type, or a reference to an interface or object literal type in the same file (though it can reference types imported from elsewhere)
- defaults must be passed via the
withDefaults
macro
- if using type-based declaration:
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
Events (emits)
- event types can be declared using a generic argument on
defineEmits
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', id: string, text: string): void
}>()
// equivalent to: defineEmits(['change', 'update'])
- explicitly type the event argument in event handler functions
- you may need to cast properties on the event
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
Provide and Inject
- for
provide
/inject
, use theInjectionKey
interface with Symbol keys
const key = Symbol() as InjectionKey<typeof foo>
provide(key, foo)
Transition/TransitionGroup
- example for fade in/out
- if the
<Transition>
component has aname
prop,v
will be replaced by the name
- if the
.v-enter-active, .v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from, .v-leave-to {
opacity: 0;
}
javascript hooks
Tip
If using JavaScript-only transitions, add the prop :css="false"
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<!-- ... -->
</Transition>
// called before the element is inserted into the DOM.
// use this to set the "enter-from" state of the element
function onBeforeEnter(el) {}
// called one frame after the element is inserted.
// use this to start the entering animation.
function onEnter(el, done) {
// call the done callback to indicate transition end
// optional if used in combination with CSS
done()
}
// called when the enter transition has finished.
function onAfterEnter(el) {}
function onEnterCancelled(el) {}
// called before the leave hook.
// Most of the time, you should just use the leave hook
function onBeforeLeave(el) {}
// called when the leave transition starts.
// use this to start the leaving animation.
function onLeave(el, done) {
// call the done callback to indicate transition end
// optional if used in combination with CSS
done()
}
// called when the leave transition has finished and the
// element has been removed from the DOM.
function onAfterLeave(el) {}
// only available with v-show transitions
function onLeaveCancelled(el) {}
- add the
appear
prop to make the transition apply on first render <Transition>
s can only apply to one element at a time, but this can include switching between elements usingv-if
- set
mode="out-in"
to make the leaving element animate out before the entering element animates in- not available for
<TransitionGroup>
- not available for
- set
- create reusable transitions by wrapping the
<Transition>
component
<template>
<Transition name="my-transition">
<slot></slot>
</Transition>
</template>
Teleport
<Teleport to="body" :disabled="isMobile">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
- the
to
target must already be in the DOM when theTeleport
component is mounted Teleport
does not affect the logical hierarchy of components, only the DOM output- if multiple
Teleport
s have the same target, the contents will be appended
Suspense (experimental) and Async Components
- to load a component async (only once it is rendered)
- props and slots on AsyncComp are passed to the inner component
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('/path/to/component.vue'),
// component to show while the async component is loading
loadingComponent: LoadingComponent,
// delay in ms before showing the loading component (default: 200)
delay: 200,
// component to show if loading fails
error: ErrorComponent,
// delay in ms before showing the error component (default: Infinity)
timeout: 3000,
})
<Suspense>
lets you display top-level loading and error states while you wait for nested async dependencies to resolve- in other words, you can have one loading spinner while you wait for multiple components to load
- supports components with an async
setup()
hook (including<script setup>
components with top-level await expressions), and async components
- async components that have a
<Suspense>
in their parent chain are treated as a dependency of it, and the async component's own loading, error, delay, & timeout options are ignored, unless it hassuspensible: false
in its options
<Suspense>
<!-- async content goes in the default slot -->
<!-- each slot can only have one immediate child -->
<Dashboard />
<!-- loading state goes in the fallback slot -->
<template #fallback>
Loading...
</template>
</Suspense>
<Suspense>
will only return to a pending state if the root node is replaced - new nested async dependencies will not trigger the fallback content again- if root node is replaced,
<Suspense>
will keep rendering the old content - to render the fallback content instead set atimeout
prop <Suspense>
emitspending
,resolve
, andfallback
events- use the
onErrorCaptured()
hook to handle async errors - must be nested in the following order:
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in">
<KeepAlive>
<Suspense>
<!-- main content -->
<component :is="Component"></component>
<!-- loading state -->
<template #fallback>
Loading...
</template>
<!-- close everything -->
Router
- to access the router in a
script setup
component- you can still use
$route
in the template
- you can still use
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
Stores (Pinia)
- define a store using
defineStore
- return anything that needs to be exposed
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++;
}
return { count, doubleCount, increment }
});
- you cannot destructure reactive properties from the store, but you can destructure actions
- use
storeToRefs
to extractref
s for each reactive property
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
console.log(count)
increment()
Important
The store is just a reactive
object, so you can modify the store state directly!
count++ // this is fine!
- to use a store inside another store,
use
it inside one of the store functions
export const useSettingsStore = defineStore('settings', () = {
async function fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
...
} else {
throw new Error('User must be authenticated')
}
}
})
Other Changes
Template
- TypeScript syntax can be used in templates
v-for
can be used onMap
s
<Item v-for="[key, value] in items">
{{value}}
</Item>
Script
- components can have multiple root nodes
- non-prop attributes must be forwarded to a specific node
nextTick
is now imported fromvue
beforeDestroy
anddestroyed
are renamed tobeforeUnmount
andunmounted
- when using composition, prefix with
on
- when using composition, prefix with
v-bind.sync
is replaced with v-model- #Provide and Inject bindings are reactive
Style
- component state can be referenced in
<style>
blocks- JavaScript expressions must be wrapped in quotes
.text {
color: v-bind(color);
}
p {
color: v-bind('theme.color');
}
- target slotted content (in the child component) using
::v-slotted()
- apply global rules in scoped style blocks using
::v-global()