Vue 3 Composition API — Master Modern Component Development

Vue 3 Composition API — Master Modern Component Development

9/29/2025 Vue By Tech Writers
VueVue 3Composition APIJavaScriptFrontend DevelopmentWeb DevelopmentVue HooksReactive Programming

Introduction: A New Way to Organize Vue Components

Vue 3 introduced the Composition API, a new way to organize and reuse component logic. Unlike the traditional Options API, the Composition API provides better organization for complex components and makes it easier to extract and reuse component logic across your application.

This comprehensive guide covers everything you need to master the Vue 3 Composition API, from basic concepts to advanced patterns.

Table of Contents

What is the Composition API?

The Composition API provides a set of functions to handle component logic, allowing you to:

  • Organize code by feature rather than by type
  • Reuse logic easily across components
  • Better support for TypeScript
  • Improved readability for complex components

Comparing APIs

// Options API
export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() { this.count++; }
  },
  computed: {
    doubled() { return this.count * 2; }
  }
}

// Composition API
import { ref, computed } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => count.value++;
    const doubled = computed(() => count.value * 2);
    
    return { count, increment, doubled };
  }
}

Setup Function

The setup() function is the entry point for the Composition API. It runs before the component is created and returns an object of properties and methods to be used in the template. This is where all your reactive state, computed properties, and methods are initialized.

import { ref, computed } from 'vue';

export default {
  setup() {
    // Reactive data
    const count = ref(0);
    const name = ref('John');
    
    // Computed properties
    const greeting = computed(() => {
      return `Hello, ${name.value}!`;
    });
    
    // Methods
    const increment = () => {
      count.value++;
    };
    
    // Return everything that should be exposed to template
    return {
      count,
      name,
      greeting,
      increment
    };
  }
}

Setup with Props and Context

The setup() function receives props and context objects as parameters, allowing you to access component props, emits, slots, and other context information passed from parent components.

import { computed } from 'vue';

export default {
  props: ['title'],
  emits: ['update'],
  setup(props, { emit, slots, expose }) {
    // Access props
    const fullTitle = computed(() => {
      return `Title: ${props.title}`;
    });
    
    // Emit events
    const handleClick = () => {
      emit('update', 'new value');
    };
    
    // Access slots
    const hasContent = computed(() => {
      return !!slots.default?.();
    });
    
    return { fullTitle, handleClick, hasContent };
  }
}

Reactive vs Ref

Understanding the difference between reactive() and ref() is crucial for mastering the Composition API.

Using Ref for Primitive Values

The ref() function is ideal for wrapping primitive values (strings, numbers, booleans). It provides reactivity by accessing the underlying value through the .value property, though this is automatic in templates.

import { ref } from 'vue';

export default {
  setup() {
    // For primitive values
    const count = ref(0);
    const message = ref('Hello');
    const isVisible = ref(false);
    
    // Must use .value to access in JavaScript
    const increment = () => {
      count.value++;
    };
    
    // But in templates, .value is automatic
    // Template: {{ count }} works directly
    
    return { count, message, isVisible, increment };
  }
}

Using Reactive for Objects

The reactive() function is better suited for complex objects with nested properties. It provides reactivity at any depth without requiring .value access in JavaScript code.

import { reactive } from 'vue';

export default {
  setup() {
    // For objects
    const state = reactive({
      count: 0,
      name: 'John',
      nested: {
        level1: {
          level2: 'deep value'
        }
      }
    });
    
    // No .value needed in JavaScript
    const increment = () => {
      state.count++;
    };
    
    // Deep properties are reactive
    const updateNested = () => {
      state.nested.level1.level2 = 'updated';
    };
    
    return { state, increment, updateNested };
  }
}

When to Use Each

// Use ref for:
const count = ref(0);                // Primitives
const items = ref([]);               // Arrays
const formData = ref({});            // Objects you want to replace

// Use reactive for:
const state = reactive({             // Complex objects
  user: { name: '', age: 0 },
  settings: { theme: 'dark' }
});

Computed Properties

Computed properties are reactive values that automatically update when their dependencies change. They’re cached based on their dependencies, making them efficient for expensive calculations.

import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('John');
    const lastName = ref('Doe');
    const age = ref(30);
    
    // Simple computed
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`;
    });
    
    // Computed with conditional
    const isAdult = computed(() => {
      return age.value >= 18;
    });
    
    // Computed with getter/setter
    const displayName = computed({
      get() {
        return `${firstName.value} ${lastName.value}`;
      },
      set(value) {
        const [first, last] = value.split(' ');
        firstName.value = first;
        lastName.value = last;
      }
    });
    
    return { firstName, lastName, age, fullName, isAdult, displayName };
  }
}

Lifecycle Hooks

Lifecycle hooks allow you to run code at specific stages of a component’s life. These hooks execute at predictable times, from before the component mounts through to when it’s unmounted, enabling you to handle initialization, data fetching, and cleanup tasks.

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

export default {
  setup() {
    onBeforeMount(() => {
      console.log('Component is about to mount');
    });
    
    onMounted(() => {
      console.log('Component mounted - fetch data here');
      // Fetch initial data
    });
    
    onBeforeUpdate(() => {
      console.log('Component is about to update');
    });
    
    onUpdated(() => {
      console.log('Component has updated');
    });
    
    onBeforeUnmount(() => {
      console.log('Component is about to unmount');
    });
    
    onUnmounted(() => {
      console.log('Component unmounted - cleanup here');
      // Clean up subscriptions, timers, etc.
    });
    
    return {};
  }
}

Watchers

Watchers enable you to run custom logic whenever a reactive value changes. They’re useful for synchronizing data, triggering API calls, or performing other side effects based on state changes.

import { ref, watch } from 'vue';

export default {
  setup() {
    const message = ref('');
    const count = ref(0);
    
    // Basic watcher
    watch(message, (newVal, oldVal) => {
      console.log(`Message changed from "${oldVal}" to "${newVal}"`);
    });
    
    // Watch multiple sources
    watch([count, message], ([newCount, newMsg]) => {
      console.log(`Count: ${newCount}, Message: ${newMsg}`);
    });
    
    // Watch with options
    watch(message, (newVal) => {
      console.log('Message:', newVal);
    }, {
      immediate: true,      // Run immediately on creation
      deep: true,           // Watch nested properties
      flush: 'post'         // Run after component updates
    });
    
    return { message, count };
  }
}

Custom Hooks (Composables)

Composables are JavaScript functions that extract and reuse component logic. They follow the naming convention use* (like useCounter, useFetch) and enable you to encapsulate and share stateful logic across multiple components.

// useCounter.js - Custom composable
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = initialValue;
  
  const doubled = computed(() => count.value * 2);
  const isEven = computed(() => count.value % 2 === 0);
  
  return {
    count,
    increment,
    decrement,
    reset,
    doubled,
    isEven
  };
}

// Using the composable
import { useCounter } from './useCounter';

export default {
  setup() {
    const { count, increment, decrement, doubled } = useCounter(0);
    
    return { count, increment, decrement, doubled };
  }
}

Template Refs

Template refs provide a way to access DOM elements or child component instances directly. They’re useful when you need to manage focus, trigger animations, or interact with third-party libraries.

import { ref, onMounted } from 'vue';

export default {
  setup() {
    const inputRef = ref(null);
    
    onMounted(() => {
      // Access the DOM element
      inputRef.value?.focus();
    });
    
    return { inputRef };
  }
}
<template>
  <input ref="inputRef" placeholder="Will auto-focus" />
</template>

Provide and Inject

Provide and Inject enable ancestor-to-descendant data sharing across the component tree without passing through intermediate components. This is useful for global configuration, themes, or shared state that multiple nested components need to access.

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

export default {
  setup() {
    const theme = ref('dark');
    const user = { id: 1, name: 'John' };
    
    provide('theme', theme);
    provide('user', user);
    
    return {};
  }
}

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

export default {
  setup() {
    const theme = inject('theme');
    const user = inject('user');
    
    return { theme, user };
  }
}

Advanced Patterns

Async Setup

You can make the setup() function async to handle asynchronous operations like data fetching before the component renders. This is often used with Suspense boundaries for better loading states.

import { defineComponent } from 'vue';

export default defineComponent({
  async setup() {
    const data = await fetchData();
    
    return { data };
  }
})

Error Handling in Composables

Implementing error handling in composables ensures that failures (like failed API requests) are captured and exposed to components, allowing them to display error states gracefully.

import { ref } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);
  
  fetch(url)
    .then(res => res.json())
    .then(result => {
      data.value = result;
    })
    .catch(err => {
      error.value = err;
    })
    .finally(() => {
      loading.value = false;
    });
  
  return { data, error, loading };
}

Best Practices

  • Organize by feature: Group related logic together
  • Extract composables: Reuse logic across components
  • Use TypeScript: Better type safety and IDE support
  • Keep setup clean: Move complex logic to separate functions
  • Document composables: Clearly document expected parameters and return values
  • Test composables: Unit test logic independently

Conclusion

The Composition API is a powerful tool for organizing Vue 3 components and making your code more maintainable and reusable. By mastering the concepts covered in this guide, you’ll be able to build scalable, well-organized Vue applications that are easy to maintain and extend.