vue-composition-api

Master the Vue 3 Composition API for building scalable, maintainable Vue applications with better code organization and reusability.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vue-composition-api" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-vue-composition-api

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) { // props is reactive console.log(props.initialCount);

// context provides attrs, slots, emit, expose
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 public methods
expose({ increment });

// Return values to template
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';

// Top-level bindings automatically exposed to template const count = ref(0); const doubled = computed(() => count.value * 2);

function increment() { count.value++; }

// Props and emits use compiler macros 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';

// Primitives const count = ref(0); const name = ref('John'); const isActive = ref(true);

// Single object that needs replacement const user = ref({ name: 'John', age: 30 }); user.value = { name: 'Jane', age: 25 }; // Works

// Arrays that need replacement const items = ref([1, 2, 3]); items.value = [4, 5, 6]; // Works

Use Reactive For

import { reactive, toRefs } from 'vue';

// Complex nested objects const state = reactive({ user: { name: 'John', age: 30 }, settings: { theme: 'dark', notifications: true }, posts: [] });

// Group related state const formState = reactive({ name: '', email: '', password: '', errors: {} });

// Convert to refs for destructuring const { name, email } = toRefs(formState);

Avoid Reactive For

// DON'T: Replacing entire reactive object loses reactivity let state = reactive({ count: 0 }); state = reactive({ count: 1 }); // Breaks reactivity!

// DO: Use ref instead const state = ref({ count: 0 }); state.value = { count: 1 }; // Works

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] || ''; } });

// Can now set 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 single source watch(count, (newValue, oldValue) => { console.log(Count changed from ${oldValue} to ${newValue}); });

// Watch multiple sources watch( [count, name], ([newCount, newName], [oldCount, oldName]) => { console.log('Multiple values changed'); } );

// Watch reactive object property const user = reactive({ name: 'John', age: 30 });

watch( () => user.name, (newName) => { console.log(Name changed to ${newName}); } );

// Deep watch 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);

// Automatically tracks dependencies watchEffect(() => { console.log(Result: ${count.value * multiplier.value}); });

// Runs immediately and whenever dependencies change

Advanced Watch Options

const data = ref(null);

watch( source, (newValue, oldValue) => { // Callback logic }, { immediate: true, // Run immediately deep: true, // Deep watch objects flush: 'post', // Timing: 'pre' | 'post' | 'sync' onTrack(e) { // Debug console.log('tracked', e); }, onTrigger(e) { // Debug console.log('triggered', e); } } );

// Stop watching const stop = watch(source, callback); stop(); // Cleanup

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');
  // DOM is available
  // Setup event listeners, fetch data
});

onBeforeUpdate(() => {
  console.log('Before update');
});

onUpdated(() => {
  console.log('Updated');
  // DOM has been updated
});

onBeforeUnmount(() => {
  console.log('Before unmount');
  // Cleanup before unmount
});

onUnmounted(() => {
  console.log('Unmounted');
  // Final cleanup
});

onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err, info);
  return false; // Stop propagation
});

// For components wrapped in &#x3C;KeepAlive>
onActivated(() => {
  console.log('Component activated');
});

onDeactivated(() => {
  console.log('Component deactivated');
});

} };

Composables - Reusable Composition Functions

Simple Composable

// composables/useCounter.ts 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 }; }

// Usage <script setup lang="ts"> import { useCounter } from '@/composables/useCounter';

const { count, doubled, increment, decrement } = useCounter(10); </script>

Advanced Composable with Side Effects

// composables/useFetch.ts 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.value = false;
}

}

watchEffect(() => { fetchData(); });

return { data: readonly(data), error: readonly(error), loading: readonly(loading), refetch: fetchData }; }

// Usage <script setup lang="ts"> import { ref } from 'vue'; import { useFetch } from '@/composables/useFetch';

const userId = ref('1'); const url = computed(() => /api/users/${userId.value}); const { data, error, loading, refetch } = useFetch(url); </script>

Composable with Cleanup

// composables/useEventListener.ts 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); }); }

// Usage <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 });

// Access props console.log(props.title); console.log(props.count);

// Destructuring loses reactivity - use toRefs import { toRefs } from 'vue'; const { title, count } = toRefs(props); </script>

TypeScript Emits

<script setup lang="ts"> // Type-safe emits 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'); // themeContext.theme // themeContext.updateTheme('light') </script>

Type-Safe Provide/Inject

// keys.ts import type { InjectionKey, Ref } from 'vue';

export interface ThemeContext { theme: Ref<string>; updateTheme: (theme: string) => void; }

export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme');

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

// Consumer <script setup lang="ts"> import { inject } from 'vue'; import { ThemeKey } from './keys';

const theme = inject(ThemeKey); // Fully typed! </script>

Provide with Default Values

<script setup lang="ts"> import { inject } from 'vue';

const theme = inject('theme', { theme: ref('light'), updateTheme: () => {} });

// Or use factory function for reactive defaults const config = inject('config', () => reactive({ locale: 'en', timezone: 'UTC' }), true); // true = treat as factory </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 ?? 'Unknown' );

async function loadUser() { isLoading.value = true; try { const response = await fetch(/api/users/${props.userId}); user.value = await response.json(); } finally { isLoading.value = false; } }

function updateUser(updates: Partial<User>) { if (user.value) { user.value = { ...user.value, ...updates }; emit('update', user.value); } } </script>

Generic Composables

// composables/useLocalStorage.ts 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>;

// Load from localStorage const stored = localStorage.getItem(key); if (stored) { try { data.value = JSON.parse(stored); } catch (e) { console.error('Failed to parse localStorage', e); } }

// Save to localStorage on change watch( data, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)); }, { deep: true } );

return data; }

// Usage 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.name) { errors.name = 'Name is required'; } else { delete errors.name; }

if (!validateEmail(form.email)) { errors.email = 'Invalid email'; } else { delete errors.email; }

if (form.password.length < 8) { errors.password = 'Password must be 8+ characters'; } else { delete errors.password; } }

async function submit() { validate(); if (!isValid.value) return;

// Submit form await fetch('/api/register', { method: 'POST', body: JSON.stringify(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>

<template> <div> <div v-if="loading">Loading...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else> <div v-for="item in data" :key="item.id"> {{ item.title }} </div> </div> </div> </template>

Resources

  • Vue 3 Composition API Documentation

  • Composition API RFC

  • VueUse - Collection of Composables

  • Vue 3 TypeScript Guide

  • Composables Best Practices

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typescript-type-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

c-systems-programming

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cpp-templates-metaprogramming

No summary provided by upstream source.

Repository SourceNeeds Review