Khaled Briki

Mastering Vue 3 Composition API: A Complete Guide

Learn how to leverage the power of Vue 3's Composition API to build more maintainable and scalable applications.

Back to all posts
Mastering Vue 3 Composition API: A Complete Guide
May 1, 2024
Khaled Briki
Vue.js, JavaScript, Frontend, Composition API

Mastering Vue 3 Composition API: A Complete Guide

The introduction of the Composition API in Vue 3 represents one of the most significant changes to the framework. It offers a more flexible and powerful way to organize component logic, especially in complex applications. This guide will help you understand and master this essential feature.

Understanding the Composition API

The Composition API was introduced to address limitations in Vue 2's Options API, particularly for reusing logic across components and for better TypeScript support. It allows you to organize code by logical concerns rather than by option types.

Setting Up a Basic Component

Let's look at a basic component using the Composition API:

<script setup>
import { ref, onMounted } from 'vue'
 
// Reactive state
const count = ref(0)
 
// Functions that mutate state and trigger updates
function increment() {
  count.value++
}
 
// Lifecycle hooks
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>
 
<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Reactivity Fundamentals

The Composition API provides several reactivity APIs:

  • ref: Creates a reactive reference for primitive values
  • reactive: Creates a reactive object
  • computed: Creates a computed property
  • watch/watchEffect: For side effects based on reactive state changes

Example of Complex State Management

<script setup>
import { reactive, computed } from 'vue'
 
const state = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 30
})
 
// Computed property
const fullName = computed(() => `${state.firstName} ${state.lastName}`)
 
// Method that modifies state
function incrementAge() {
  state.age++
}
</script>
 
<template>
  <div>
    <p>Name: {{ fullName }}</p>
    <p>Age: {{ state.age }}</p>
    <button @click="incrementAge">Increment Age</button>
  </div>
</template>

Composables: Reusable Logic

One of the biggest advantages of the Composition API is the ability to extract and reuse logic through "composables" - functions that encapsulate and return reactive state and methods.

Creating a Simple Composable

// useCounter.js
import { ref } from 'vue'
 
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  return {
    count,
    increment,
    decrement
  }
}

Using the Composable in Components

<script setup>
import { useCounter } from './useCounter'
 
const { count, increment, decrement } = useCounter(10)
</script>
 
<template>
  <div>
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

Template Refs

Accessing DOM elements is straightforward with the Composition API:

<script setup>
import { ref, onMounted } from 'vue'
 
const inputRef = ref(null)
 
onMounted(() => {
  inputRef.value.focus()
})
</script>
 
<template>
  <input ref="inputRef" />
</template>

Working with Props

Handling props is slightly different with the Composition API:

<script setup>
import { computed } from 'vue'
 
// Define props with types and defaults
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})
 
// Computed property based on props
const capitalizedTitle = computed(() => 
  props.title.charAt(0).toUpperCase() + props.title.slice(1)
)
 
// Define events
const emit = defineEmits(['update:count'])
 
function incrementCount() {
  emit('update:count', props.count + 1)
}
</script>
 
<template>
  <div>
    <h2>{{ capitalizedTitle }}</h2>
    <p>Count: {{ count }}</p>
    <button @click="incrementCount">Increment</button>
  </div>
</template>

Advanced Patterns

Async Operations

<script setup>
import { ref } from 'vue'
 
const userData = ref(null)
const loading = ref(false)
const error = ref(null)
 
async function fetchUserData(userId) {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`)
    userData.value = await response.json()
  } catch (e) {
    error.value = e.message
  } finally {
    loading.value = false
  }
}
</script>

Creating Renderless Components

The Composition API makes it easier to create renderless components:

<!-- FetchData.vue -->
<script setup>
import { ref, onMounted, provide } from 'vue'
 
const props = defineProps({
  url: String,
  immediate: {
    type: Boolean,
    default: true
  }
})
 
const data = ref(null)
const loading = ref(false)
const error = ref(null)
 
async function fetchData() {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch(props.url)
    data.value = await response.json()
  } catch (e) {
    error.value = e
  } finally {
    loading.value = false
  }
}
 
// Provide context to slot
provide('fetchState', {
  data,
  loading,
  error,
  refetch: fetchData
})
 
onMounted(() => {
  if (props.immediate) {
    fetchData()
  }
})
</script>
 
<template>
  <slot :data="data" :loading="loading" :error="error" :refetch="fetchData" />
</template>

Usage:

<template>
  <FetchData url="https://api.example.com/posts">
    <template v-slot="{ data, loading, error, refetch }">
      <div v-if="loading">Loading...</div>
      <div v-else-if="error">Error: {{ error.message }}</div>
      <div v-else>
        <ul>
          <li v-for="post in data" :key="post.id">{{ post.title }}</li>
        </ul>
        <button @click="refetch">Refresh</button>
      </div>
    </template>
  </FetchData>
</template>

Conclusion

The Composition API offers a more flexible way to organize component logic, especially for complex applications. By focusing on function-based composition rather than options-based organization, you can create more maintainable and reusable code.

Key takeaways:

  • Use ref for primitive values and reactive for objects
  • Extract reusable logic into composables
  • Leverage lifecycle hooks with function-based approach
  • Take advantage of TypeScript support

With these fundamentals in place, you're well on your way to mastering Vue 3's Composition API and building more scalable applications.

About the Author

Khaled Briki

Khaled Briki

Web Developer

Full-stack web developer with expertise in Vue.js, Laravel, and modern JavaScript frameworks.