| name | vue-composition-api |
| user-invocable | false |
| description | Use when Vue 3 Composition API with reactive refs, computed, and composables. Use when building modern Vue 3 applications. |
| allowed-tools | ["Bash","Read"] |
Vue Composition API
Master the Vue 3 Composition API for building scalable, maintainable
Vue applications with better code organization and reusability.
Setup Function Fundamentals
The setup() function is the entry point for using the Composition API:
import { ref, computed, onMounted } from 'vue';
export default {
props: ['initialCount'],
setup(props, context) {
console.log(props.initialCount);
const { attrs, slots, emit, expose } = context;
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
emit('update', count.value);
}
onMounted(() => {
console.log('Component mounted');
});
expose({ increment });
return {
count,
doubled,
increment
};
}
};
Script Setup Syntax
Modern Vue 3 uses <script setup> for cleaner syntax:
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
interface Props {
initialCount?: number;
}
const props = withDefaults(defineProps<Props>(), {
initialCount: 0
});
const emit = defineEmits<{
update: [value: number];
}>();
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>
Ref vs Reactive - When to Use Each
Use Ref For
import { ref } from 'vue';
const count = ref(0);
const name = ref('John');
const isActive = ref(true);
const user = ref({ name: 'John', age: 30 });
user.value = { name: 'Jane', age: 25 };
const items = ref([1, 2, 3]);
items.value = [4, 5, 6];
Use Reactive For
import { reactive, toRefs } from 'vue';
const state = reactive({
user: { name: 'John', age: 30 },
settings: { theme: 'dark', notifications: true },
posts: []
});
const formState = reactive({
name: '',
email: '',
password: '',
errors: {}
});
const { name, email } = toRefs(formState);
Avoid Reactive For
let state = reactive({ count: 0 });
state = reactive({ count: 1 });
const state = ref({ count: 0 });
state.value = { count: 1 };
Computed Properties Patterns
Basic Computed
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
Writable Computed
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const names = value.split(' ');
firstName.value = names[0] || '';
lastName.value = names[1] || '';
}
});
fullName.value = 'Jane Smith';
Computed with Complex Logic
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
const cart = ref<Product[]>([]);
const cartSummary = computed(() => {
const total = cart.value.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const itemCount = cart.value.reduce((sum, item) =>
sum + item.quantity, 0
);
const tax = total * 0.08;
const grandTotal = total + tax;
return {
total,
itemCount,
tax,
grandTotal
};
});
Watch and WatchEffect
Watch - Explicit Dependencies
import { ref, watch } from 'vue';
const count = ref(0);
const name = ref('');
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
watch(
[count, name],
([newCount, newName], [oldCount, oldName]) => {
console.log('Multiple values changed');
}
);
const user = reactive({ name: 'John', age: 30 });
watch(
() => user.name,
(newName) => {
console.log(`Name changed to ${newName}`);
}
);
watch(
user,
(newUser) => {
console.log('User changed:', newUser);
},
{ deep: true }
);
WatchEffect - Auto Tracking
import { ref, watchEffect } from 'vue';
const count = ref(0);
const multiplier = ref(2);
watchEffect(() => {
console.log(`Result: ${count.value * multiplier.value}`);
});
Advanced Watch Options
const data = ref(null);
watch(
source,
(newValue, oldValue) => {
},
{
immediate: true,
deep: true,
flush: 'post',
onTrack(e) {
console.log('tracked', e);
},
onTrigger(e) {
console.log('triggered', e);
}
}
);
const stop = watch(source, callback);
stop();
Lifecycle Hooks in Composition API
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('Before mount');
});
onMounted(() => {
console.log('Mounted');
});
onBeforeUpdate(() => {
console.log('Before update');
});
onUpdated(() => {
console.log('Updated');
});
onBeforeUnmount(() => {
console.log('Before unmount');
});
onUnmounted(() => {
console.log('Unmounted');
});
onErrorCaptured((err, instance, info) => {
console.(, err, info);
;
});
( {
.();
});
( {
.();
});
}
};
Composables - Reusable Composition Functions
Simple Composable
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function reset() {
count.value = initialValue;
}
return {
count: readonly(count),
doubled,
increment,
decrement,
reset
};
}
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';
const { count, doubled, increment, decrement } = useCounter(10);
</script>
Advanced Composable with Side Effects
import { ref, unref, watchEffect } from 'vue';
import type { Ref } from 'vue';
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(unref(url));
if (!response.ok) throw new Error('Fetch failed');
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading. = ;
}
}
( {
();
});
{
: (data),
: (error),
: (loading),
: fetchData
};
}
<script setup lang=>
{ ref } ;
{ useFetch } ;
userId = ();
url = ( );
{ data, error, loading, refetch } = (url);
</script>
Composable with Cleanup
import { onMounted, onUnmounted } from 'vue';
export function useEventListener(
target: EventTarget,
event: string,
handler: (e: Event) => void
) {
onMounted(() => {
target.addEventListener(event, handler);
});
onUnmounted(() => {
target.removeEventListener(event, handler);
});
}
<script setup lang="ts">
import { useEventListener } from '@/composables/useEventListener';
useEventListener(window, 'resize', () => {
console.log('Window resized');
});
</script>
Props and Emits in Composition API
TypeScript Props
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
user: {
name: string;
email: string;
};
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
console.log(props.title);
console.log(props.count);
import { toRefs } from 'vue';
const { title, count } = toRefs(props);
</script>
TypeScript Emits
<script setup lang="ts">
const emit = defineEmits<{
update: [value: number];
delete: [];
change: [id: string, value: string];
}>();
function handleUpdate() {
emit('update', 42);
}
function handleChange(id: string, value: string) {
emit('change', id, value);
}
</script>
Runtime Props Validation
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator: (value: number) => value >= 0
},
status: {
type: String as PropType<'active' | 'inactive'>,
default: 'active'
}
});
</script>
Provide and Inject Patterns
Basic Provide/Inject
<!-- Parent Component -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide('theme', { theme, updateTheme });
</script>
<!-- Child Component (any depth) -->
<script setup lang="ts">
import { inject } from 'vue';
const themeContext = inject('theme');
</script>
Type-Safe Provide/Inject
import type { InjectionKey, Ref } from 'vue';
export interface ThemeContext {
theme: Ref<string>;
updateTheme: (theme: string) => void;
}
export const ThemeKey: InjectionKey<ThemeContext> =
Symbol('theme');
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ThemeKey } from './keys';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide(ThemeKey, { theme, updateTheme });
</script>
<script =>
Provide with Default Values
<script setup lang="ts">
import { inject } from 'vue';
const theme = inject('theme', {
theme: ref('light'),
updateTheme: () => {}
});
const config = inject('config', () => reactive({
locale: 'en',
timezone: 'UTC'
}), true);
</script>
TypeScript with Composition API
Component with Full Types
<script setup lang="ts">
import { ref, computed, type Ref, type ComputedRef } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
userId: number;
}
interface Emits {
(e: 'update', user: User): void;
(e: 'delete', id: number): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const user: Ref<User | null> = ref(null);
const isLoading = ref(false);
const userName: ComputedRef<string> = computed(() =>
user.value?.name ??
);
() {
isLoading. = ;
{
response = ();
user. = response.();
} {
isLoading. = ;
}
}
() {
(user.) {
user. = { ...user., ...updates };
(, user.);
}
}
</script>
Generic Composables
import { ref, watch, type Ref } from 'vue';
export function useLocalStorage<T>(
key: string,
defaultValue: T
): Ref<T> {
const data = ref<T>(defaultValue) as Ref<T>;
const stored = localStorage.getItem(key);
if (stored) {
try {
data.value = JSON.parse(stored);
} catch (e) {
console.error('Failed to parse localStorage', e);
}
}
watch(
data,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true }
);
return data;
}
const user = useLocalStorage<User>('user', { id: 0, name: '' });
When to Use This Skill
Use vue-composition-api when building modern, production-ready
applications that require:
- Complex component logic that benefits from better organization
- Reusable logic across multiple components (composables)
- Better TypeScript integration and type inference
- Fine-grained reactivity control
- Large-scale applications requiring maintainability
- Migration from Vue 2 Options API to Vue 3
- Sharing stateful logic without mixins
Vue-Specific Best Practices
- Prefer
<script setup> syntax - Cleaner, better performance, better types
- Use composables for reusable logic - Extract to
composables/ directory
- Use
ref for primitives, reactive for objects - Unless you need to
replace objects
- Always use TypeScript - Better DX and fewer runtime errors
- Destructure reactive objects with
toRefs - Preserve reactivity
- Use computed for derived state - Not methods in templates
- Cleanup side effects - Use
onUnmounted for event listeners, timers
- Keep components focused - Extract complex logic to composables
- Use provide/inject for deep prop passing - Avoid prop drilling
- Name composables with
use prefix - Follow convention (useCounter, useFetch)
Vue-Specific Pitfalls
- Destructuring props directly - Loses reactivity, use
toRefs(props)
- Forgetting
.value on refs - Common source of bugs
- Mutating props - Props are readonly, emit events instead
- Using reactive() for entire state - Can't replace, use ref for root
- Not cleaning up watchers - Memory leaks, store stop handle
- Accessing refs before mount - DOM refs are null in setup
- Overusing reactive() - Use ref for simple values
- Not using computed for derived state - Recalculates on every render
- Forgetting to return from setup() - Without
<script setup>
- Mixing Options API and Composition API - Confusing, pick one
Common Patterns
Form Handling
<script setup lang="ts">
import { reactive, computed } from 'vue';
interface FormData {
name: string;
email: string;
password: string;
}
interface FormErrors {
name?: string;
email?: string;
password?: string;
}
const form = reactive<FormData>({
name: '',
email: '',
password: ''
});
const errors = reactive<FormErrors>({});
const isValid = computed(() =>
Object.keys(errors).length === 0 &&
form.name && form.email && form.password
);
function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validate() {
if (!form.) {
errors. = ;
} {
errors.;
}
(!(form.)) {
errors. = ;
} {
errors.;
}
(form.. < ) {
errors. = ;
} {
errors.;
}
}
() {
();
(!isValid.) ;
(, {
: ,
: .(form)
});
}
</script>
Async Data Loading
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface Data {
id: number;
title: string;
}
const data = ref<Data[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
data.value = await response.json();
} catch (e) {
error.value = (e as Error).message;
} finally {
loading.value = false;
}
}
onMounted(() => {
fetchData();
});
</script>
Resources