Vue 3

Composition API

<script setup lang="ts">
// code goes here!
</script>

Why use composition?

The Composition API is recommended over mixins.

Reactive State & Refs

Note

Use ref for state that will be replaced wholesale, and reactive for object types that hold many individual state items.

import { ref } from 'vue'

const count = ref(0)

console.log(count.value)

// deep reactivity
const objRef = ref({ count: 0 })
objRef.value.count++
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>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
    state.count++
}
/* 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)
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

import { reactive, computed } from 'vue'

const person = reactive({ firstName: 'John', lastName: 'Smith' })

const fullName = computed(() => {
    return person.firstName + ' ' + person.lastName
})

console.log(fullName.value)
const fullName = computed({
    get() {
        ...
    },
    set(newValue) {
        ...
    }
})

Lifecycle Hooks

import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => { ... })

// this won't work
setTimeout(() => {
    onMounted(() => { ... })
}, 100)

Watchers

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) => { ... })
const x = ref(0)
const y = ref(0)

watch([x, y], ([newX, newY]) => { ... })
watch(
    () => state.someObject,
    (newValue, oldValue) => {
        // if deep is not used, won't trigger unless state.someObject is replaced
    },
    { deep: true }
)
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()
})
const unwatch = watchEffect(() => { ... })
unwatch() // later
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

<input ref="input">
import { ref } from 'vue'
const input = ref(null)
console.log(input.value)
<li v-for="item in list" ref="itemRefs">{{ item }}</li>
const list = ref([ ... ])
const itemRefs = ref([])
<input :ref="(el) => { /* assign el to a property or ref */ }">
/* 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

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

const emit = defineEmits({
    text: String,
    click: null, // no validation
    submit({ email, password }) => {
        return email && password
    }
})

emit('submit', { email, password })
<ChildComponent v-model:title="pageTitle" />

<!-- is shorthand for: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

Fallthrough Attributes

<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>

Provide and Inject

import { provide } from 'vue'
provide('message', 'hello!')

// in another component

import { inject } from 'vue'
const message = inject('message')
const value = inject('message', 'default value')
const value2 = inject('key', () => new ExpensiveClass())
// provider component
const location = ref('North Pole')
function updateLocation(newLocation) {
    location.value = newLocation
}
provide('location', { location, updateLocation })

// injector component
const { location, updateLocation } = inject('location')
import { ref, provide, readonly } from 'vue'
const count = ref(0)

provide('read-only-count', readonly(count))

Composables

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)
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
    onMounted(() => target.addEventListener(event, callback))
    onUnmounted(() => target.removeEventListener(event, callback))
}
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

<script setup>
const vFocus = {
    mounted: el => el.focus()
    //// other hooks:
    // created
    // beforeMount
    // beforeUpdate
    // updated
    // beforeUnmount
    // unmounted
}
</script>

<template>
    <input v-focus />
</template>
<div v-example:[arg]="value"></div>
const vColor = (el, binding) => {
    el.style.color = binding.value
}

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

const year = ref<string | number>('2020')

const double = computed<number>(() => { ... })
<script setup lang="ts">
const el = ref<HTMLInputElement | null>(null)
</script>

<template>
    <input ref="el" />
</template>
const modal = ref<InstanceType<typeof MyModal> | null>(null)

Reactive

interface Book {
    title: string
    year?: number
}
const book: Book = reactive({ title: 'Vue 3 Guide' })

Props

export interface Props {
    msg?: string
    labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
    msg: 'hello',
    labels: () => ['one', 'two']
})

Events (emits)

const emit = defineEmits<{
    (e: 'change', id: number): void
    (e: 'update', id: string, text: string): void
}>()
// equivalent to: defineEmits(['change', 'update'])
function handleChange(event: Event) {
    console.log((event.target as HTMLInputElement).value)
}

Provide and Inject

const key = Symbol() as InjectionKey<typeof foo>
provide(key, foo)

Transition/TransitionGroup

transition-classes.f0f7b3c9.png

.v-enter-active, .v-leave-active {
    transition: opacity 0.5s ease;
}

.v-enter-from, .v-leave-to {
    opacity: 0;
}
<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>

Suspense (experimental) and Async Components

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>
    <!-- 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>
<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

import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

Stores (Pinia)

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 }
});
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!
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

<Item v-for="[key, value] in items">
    {{value}}
</Item>

Script

Style

.text {
  color: v-bind(color);
}

p {
  color: v-bind('theme.color');
}

See also