← Back to Guides

Vue.js 3 Composition API Complete Tutorial

📖 13 min read | 📅 Updated: January 2025 | 🏷️ Web Development

Introduction to Composition API

Vue 3's Composition API provides a new way to organize and reuse logic in Vue components. It offers better TypeScript support, improved code organization, and easier logic reuse compared to the Options API.

1. Basic Setup

<script setup>
import { ref, computed, onMounted } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);

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

onMounted(() => {
  console.log('Component mounted');
});
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

2. Reactive State

import { ref, reactive, toRefs } from 'vue';

// ref - for primitive values
const count = ref(0);
const message = ref('Hello');

// reactive - for objects
const state = reactive({
  user: { name: 'John', age: 30 },
  isLoading: false,
  errors: []
});

// Destructure with toRefs
const { user, isLoading } = toRefs(state);

// Access values
count.value = 5;
state.user.name = 'Jane';

3. Computed Properties

import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// Read-only computed
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

// Writable computed
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ');
  }
});

4. Watchers

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

const question = ref('');
const answer = ref('');

// Watch single ref
watch(question, async (newQuestion) => {
  if (newQuestion.includes('?')) {
    answer.value = 'Thinking...';
    answer.value = await fetchAnswer(newQuestion);
  }
});

// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(`Name changed to ${newFirst} ${newLast}`);
});

// watchEffect - runs immediately
watchEffect(() => {
  console.log(`Question: ${question.value}`);
  console.log(`Answer: ${answer.value}`);
});

// Stop watching
const stop = watch(question, callback);
stop(); // Clean up

5. Lifecycle Hooks

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue';

onBeforeMount(() => {
  console.log('Before mount');
});

onMounted(() => {
  console.log('Mounted');
  // Fetch data, setup listeners
});

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

onUpdated(() => {
  console.log('Updated');
});

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

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

6. Composables (Custom Hooks)

// composables/useFetch.js
import { ref } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  async function fetch() {
    loading.value = true;
    try {
      const response = await fetch(url);
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  return { data, error, loading, fetch };
}

// Usage in component
import { useFetch } from './composables/useFetch';

const { data, error, loading, fetch } = useFetch('/api/users');
fetch();

7. Template Refs

<script setup>
import { ref, onMounted } from 'vue';

const inputEl = ref(null);

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

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

8. Provide / Inject

// Parent component
import { provide, ref } from 'vue';

const theme = ref('dark');

function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark';
}

provide('theme', { theme, toggleTheme });

// Child component (any level)
import { inject } from 'vue';

const { theme, toggleTheme } = inject('theme');

9. Props and Emits with TypeScript

<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
});

interface Emits {
  (e: 'update', value: number): void;
  (e: 'delete'): void;
}

const emit = defineEmits<Emits>();

function handleClick() {
  emit('update', props.count + 1);
}
</script>

10. Async Components

import { defineAsyncComponent } from 'vue';

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
);

// With options
const AsyncCompWithOptions = defineAsyncComponent({
  loader: () => import('./components/Heavy.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
});
💡 Best Practices:

Conclusion

The Composition API is the future of Vue development, offering better code organization, TypeScript support, and reusability. By mastering these patterns, you'll write more maintainable and scalable Vue applications.