Skip to main content
Technology & EngineeringVue177 lines

Composition API

Patterns and best practices for Vue 3 Composition API including refs, reactive, computed, watchers, and lifecycle hooks

Quick Summary18 lines
You are an expert in Vue 3 Composition API patterns for building scalable, maintainable Vue applications.

## Key Points

- **Watchers That Modify Their Own Source** — creating `watch` callbacks that mutate the value they are watching without a guard condition, causing infinite update loops that crash the browser tab.
- **Accessing Template Refs at Script Top Level** — reading `ref.value` on a template ref before `onMounted`, which always returns `null`. Template refs are only populated after the component mounts.
- **`ref`** — wraps a single value in a reactive container; access via `.value` in script, auto-unwrapped in templates
- **`reactive`** — creates a deeply reactive proxy of an object; no `.value` needed but cannot reassign the root
- **`shallowRef` / `shallowReactive`** — reactive only at the top level, useful for large objects or external data
- **`toRefs` / `toRef`** — converts reactive object properties to individual refs while preserving reactivity
- **`computed`** — derives a cached reactive value that updates only when dependencies change
- **`watch`** — watches one or more reactive sources and runs a callback on change
- **`watchEffect`** — runs a side effect immediately, auto-tracking all reactive dependencies accessed during execution
1. **Prefer `<script setup>`** — it is more concise, has better TypeScript inference, and better runtime performance than the Options API or raw `setup()` function.
2. **Use `ref` for primitives, `reactive` for grouped state** — avoid mixing them inconsistently within a component.
3. **Extract logic into composables** — when a component grows beyond ~100 lines of script, split logical concerns into composable functions.
skilldb get vue-skills/Composition APIFull skill: 177 lines
Paste into your CLAUDE.md or agent config

Composition API — Vue.js

You are an expert in Vue 3 Composition API patterns for building scalable, maintainable Vue applications.

Core Philosophy

The Composition API exists to solve a specific problem: as components grow, option-based organization (data, methods, computed, watch) scatters related logic across the file, making it hard to follow a single concern. The Composition API flips this by letting you organize code by logical concern — all the search logic together, all the pagination logic together — regardless of whether it involves state, computed values, or lifecycle hooks. This is not just a stylistic preference; it fundamentally changes how you reason about component complexity.

Reactivity in Vue 3 is explicit and granular. You choose ref for individual values and reactive for grouped objects, you explicitly declare what to watch, and you explicitly define computed derivations. This explicitness is a feature: it makes the reactive dependency graph visible in the code rather than hidden behind framework magic. When something updates, you can trace exactly why by following the refs and watchers.

The <script setup> syntax is the canonical way to write Composition API code. It removes ceremony, improves TypeScript inference, and compiles to more efficient code. Treating it as the default rather than an alternative is important — it is the API surface that Vue's tooling, documentation, and ecosystem are optimized for.

Anti-Patterns

  • Mixing Options API and Composition API Arbitrarily — using data() and setup() in the same component without a migration rationale creates confusion about where state lives and how reactivity flows. Commit to one approach per component.

  • Overusing reactive for Everything — reaching for reactive({}) when individual ref values would be clearer. Reactive objects cannot be reassigned at the root and lose reactivity when destructured, making them subtly error-prone for simple state.

  • Watchers That Modify Their Own Source — creating watch callbacks that mutate the value they are watching without a guard condition, causing infinite update loops that crash the browser tab.

  • Forgetting Cleanup in watchEffect — setting up timers, event listeners, or abort controllers inside watchEffect without returning a cleanup function from onCleanup, leading to resource leaks every time the effect re-runs.

  • Accessing Template Refs at Script Top Level — reading ref.value on a template ref before onMounted, which always returns null. Template refs are only populated after the component mounts.

Overview

The Composition API is Vue 3's primary API for organizing component logic by logical concern rather than option type. It provides fine-grained reactivity primitives (ref, reactive, computed, watch) and lifecycle hooks that can be composed into reusable functions.

Core Concepts

Reactivity Primitives

  • ref — wraps a single value in a reactive container; access via .value in script, auto-unwrapped in templates
  • reactive — creates a deeply reactive proxy of an object; no .value needed but cannot reassign the root
  • shallowRef / shallowReactive — reactive only at the top level, useful for large objects or external data
  • toRefs / toRef — converts reactive object properties to individual refs while preserving reactivity

Computed and Watchers

  • computed — derives a cached reactive value that updates only when dependencies change
  • watch — watches one or more reactive sources and runs a callback on change
  • watchEffect — runs a side effect immediately, auto-tracking all reactive dependencies accessed during execution

Lifecycle Hooks

onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated, onErrorCaptured

Implementation Patterns

Basic Component with <script setup>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([])
const search = ref('')

const filteredUsers = computed(() =>
  users.value.filter(u =>
    u.name.toLowerCase().includes(search.value.toLowerCase())
  )
)

onMounted(async () => {
  const res = await fetch('/api/users')
  users.value = await res.json()
})
</script>

<template>
  <input v-model="search" placeholder="Search users..." />
  <ul>
    <li v-for="user in filteredUsers" :key="user.id">
      {{ user.name }} — {{ user.email }}
    </li>
  </ul>
</template>

Reactive Object vs Ref

import { ref, reactive } from 'vue'

// Use ref for primitives and values you may reassign
const count = ref(0)
count.value++

// Use reactive for objects where you mutate properties
const form = reactive({
  name: '',
  email: '',
  errors: {} as Record<string, string>,
})
form.name = 'Alice'

// Avoid: reassigning a reactive root loses reactivity
// form = { name: 'Bob', email: '', errors: {} } // WRONG
Object.assign(form, { name: 'Bob', email: '', errors: {} }) // OK

Watcher Patterns

import { ref, watch, watchEffect } from 'vue'

const query = ref('')
const debouncedQuery = ref('')

// Watch a specific source with options
watch(query, (newVal, oldVal) => {
  console.log(`Changed from "${oldVal}" to "${newVal}"`)
}, { immediate: false })

// Watch multiple sources
watch([query, debouncedQuery], ([q, dq]) => {
  console.log(q, dq)
})

// watchEffect auto-tracks dependencies
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    debouncedQuery.value = query.value
  }, 300)
  onCleanup(() => clearTimeout(timer))
})

Template Refs

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const inputEl = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputEl.value?.focus()
})
</script>

<template>
  <input ref="inputEl" />
</template>

Best Practices

  1. Prefer <script setup> — it is more concise, has better TypeScript inference, and better runtime performance than the Options API or raw setup() function.
  2. Use ref for primitives, reactive for grouped state — avoid mixing them inconsistently within a component.
  3. Extract logic into composables — when a component grows beyond ~100 lines of script, split logical concerns into composable functions.
  4. Type your refs — use ref<Type>() generics rather than relying on inference for complex types.
  5. Use watchEffect for syncing side effects and watch when you need the old value or explicit source control.
  6. Return cleanup from watchEffect via onCleanup to prevent memory leaks from timers, subscriptions, or abort controllers.
  7. Avoid deep watchers on large objects — use shallowRef or watch specific properties instead.

Common Pitfalls

  • Forgetting .value — in <script>, refs require .value; in templates they do not. TypeScript helps catch this but it remains the most common mistake.
  • Destructuring reactive objectsconst { name } = reactive({ name: 'A' }) loses reactivity. Use toRefs() to destructure safely.
  • Reassigning reactive rootsstate = newObj replaces the proxy. Use Object.assign or replace individual properties.
  • Infinite watch loops — mutating a watched source inside its own watcher without a guard condition causes infinite recursion.
  • Accessing template refs before mount — template refs are null until onMounted; never access them at the top level of <script setup>.
  • Forgetting to handle async in watchEffectwatchEffect(async () => { ... }) does not track dependencies after the first await. Declare reactive reads before any await.

Install this skill directly: skilldb add vue-skills

Get CLI access →