Composition API
The Composition API is Vue 3’s primary way to organise component logic. Instead of spreading code across data, computed, methods, and watch, you write standalone reactive primitives that can be extracted into reusable composables.
setup, reactivity, composables, lifecycle
EXAMPLE
<script setup lang="ts">
import { ref, reactive, computed, watch, watchEffect, onMounted, onUnmounted } from 'vue';
// 1) Reactive primitives
const count = ref(0); // primitive — needs .value in JS
const state = reactive({ name: 'Mara', age: 30 }); // object — no .value
const doubled = computed(() => count.value * 2);
function inc() { count.value++; }
// 2) Lifecycle hooks
onMounted(() => console.log('mounted'));
onUnmounted(() => console.log('cleanup'));
// 3) Watchers
watch(count, (n, old) => console.log(`went ${old} → ${n}`));
watch([count, () => state.name], ([c, n]) => { /* multi-source */ });
watchEffect(() => console.log(`count is ${count.value}`)); // immediate + auto-tracks deps
// 4) Props + emits — type-safe
const props = defineProps<{ title: string; count?: number }>();
const emit = defineEmits<{ (e: 'change', value: number): void }>();
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count {{ count }} (doubled {{ doubled }})</p>
<button @click="inc">+1</button>
</div>
</template>
<!-- 5) Composables — extract reusable reactive logic -->
<script lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0), y = ref(0);
function update(e: MouseEvent) { x.value = e.clientX; y.value = e.clientY; }
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
</script>
<!-- Consumer -->
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse';
const { x, y } = useMouse();
</script>
<template><p>{{ x }}, {{ y }}</p></template>
<!-- 6) Reactivity gotchas -->
<script setup lang="ts">
import { reactive, toRefs } from 'vue';
const state = reactive({ a: 1, b: 2 });
const { a, b } = state; // ❌ loses reactivity
const { a, b } = toRefs(state); // ✓ each is a ref
</script>
<!-- 7) Async setup + <Suspense> -->
<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json());
</script>
<!-- Parent: <Suspense><MyAsyncComp /></Suspense> -->
<!-- 8) Provide / inject — context-style dependency injection -->
<script setup lang="ts">
import { provide, inject, ref } from 'vue';
import type { InjectionKey } from 'vue';
interface Theme { mode: 'light' | 'dark'; toggle(): void; }
export const ThemeKey: InjectionKey<Theme> = Symbol('Theme');
const mode = ref<'light' | 'dark'>('light');
provide(ThemeKey, { mode: mode as any, toggle: () => mode.value = mode.value === 'light' ? 'dark' : 'light' });
</script>
<!-- Descendant -->
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from '@/components/ThemeProvider.vue';
const theme = inject(ThemeKey);
if (!theme) throw new Error('ThemeProvider missing');
</script>
<!-- 9) Refs to DOM elements -->
<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>
<!-- 10) Composition API vs Options API -->
<!-- Options API: code split by KIND (data, methods, computed, watch) — fine for small components -->
<!-- Composition API: code split by CONCERN (use* composables) — scales better; preferred for new code -->
<!-- 11) defineModel (Vue 3.4+) — clean two-way binding -->
<script setup lang="ts">
const model = defineModel<string>(); // ref<string | undefined>
const count = defineModel<number>('count', { default: 0 });
</script>
<template><input v-model="model" /></template>
<!-- 12) Common bugs -->
<!-- • Forgot .value in JS for refs → 'undefined' or NaN -->
<!-- • Destructured reactive() → lost reactivity; use toRefs -->
<!-- • watch source as a VALUE (state.x) instead of a getter (() => state.x) -->
<!-- • Async work AFTER first await in setup() — only deps before the first await are tracked -->
<!-- • Storing complex 3rd-party object in reactive() → can break the object's internals; use shallowRef -->
<!-- • Mutating a prop → warning; emit an event instead -->
<!-- • Cleanup forgotten in composable — leaks listeners; always pair onMounted with onUnmounted -->
Why it matters
Composition API organises code by concern instead of by kind: useX composables you can extract, test, and share. Reach for ref for primitives, reactive for objects (plus toRefs when destructuring), and composables for anything you’d duplicate across components.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<script setup>
import { ref, onMounted } from 'vue';
const count = ref(0);
onMounted(() => console.log('mounted'));
</script>
Try it Yourself »
Discussion
Loading…