12 min read

Mastering Vue 3 Composition API

A comprehensive guide to Vue 3's Composition API, covering reactivity, lifecycle hooks, and composables with practical examples.

Vue.jsComposition APIJavaScriptFrontend

Mastering Vue 3 Composition API

The Composition API is one of the most significant additions to Vue 3, offering a new way to organize and reuse logic in Vue components. Unlike the Options API, the Composition API allows you to group related logic together, making your code more maintainable and reusable.

Why Composition API?

Better Logic Reuse

With the Options API, sharing logic between components often required mixins or higher-order components, which could lead to naming conflicts and unclear data sources. The Composition API solves this with composables:

// useCounter.js
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 isPositive = computed(() => count.value > 0)
  
  return {
    count,
    increment,
    decrement,
    reset,
    isPositive
  }
}

Better TypeScript Support

The Composition API provides superior TypeScript integration:

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([])
const selectedUser = ref<User | null>(null)

// TypeScript infers types automatically
const userName = computed(() => selectedUser.value?.name ?? 'No user selected')
</script>

Core Concepts

Reactivity

Vue 3 introduces new reactivity primitives:

import { ref, reactive, computed, watch } from 'vue'

// Reactive references
const count = ref(0)
const message = ref('Hello')

// Reactive objects
const state = reactive({
  user: null,
  loading: false,
  error: null
})

// Computed properties
const doubleCount = computed(() => count.value * 2)

// Watchers
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

Lifecycle Hooks

Lifecycle hooks in the Composition API are imported functions:

<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'

onMounted(() => {
  console.log('Component mounted')
})

onUpdated(() => {
  console.log('Component updated')
})

onUnmounted(() => {
  console.log('Component unmounted')
})
</script>

Practical Examples

Form Handling

Create a reusable form composable:

// useForm.js
import { ref, reactive, computed } from 'vue'

export function useForm(initialValues = {}, validationRules = {}) {
  const values = reactive({ ...initialValues })
  const errors = ref({})
  const touched = ref({})
  
  const isValid = computed(() => 
    Object.keys(errors.value).length === 0
  )
  
  const validate = (field) => {
    if (validationRules[field]) {
      const error = validationRules[field](values[field])
      if (error) {
        errors.value[field] = error
      } else {
        delete errors.value[field]
      }
    }
  }
  
  const handleChange = (field, value) => {
    values[field] = value
    touched.value[field] = true
    validate(field)
  }
  
  const handleSubmit = (onSubmit) => {
    // Validate all fields
    Object.keys(values).forEach(validate)
    
    if (isValid.value) {
      onSubmit(values)
    }
  }
  
  return {
    values,
    errors,
    touched,
    isValid,
    handleChange,
    handleSubmit
  }
}

API Data Fetching

Create a flexible data fetching composable:

// useApi.js
import { ref, computed } from 'vue'

export function useApi(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const isError = computed(() => error.value !== null)
  const isEmpty = computed(() => !loading.value && !data.value)
  
  const execute = async () => {
    try {
      loading.value = true
      error.value = null
      
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = null
    error.value = null
    loading.value = false
  }
  
  return {
    data,
    loading,
    error,
    isError,
    isEmpty,
    execute,
    reset
  }
}

Migration from Options API

Here's how a typical Options API component looks when converted:

<!-- Options API -->
<script>
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('Component mounted')
  }
}
</script>
<!-- Composition API -->
<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const message = ref('Hello')

const doubleCount = computed(() => count.value * 2)

const increment = () => count.value++

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

Best Practices

1. Use <script setup>

The <script setup> syntax is more concise and provides better performance:

<script setup>
// More concise than the traditional composition API syntax
const count = ref(0)
const increment = () => count.value++
</script>

2. Extract Reusable Logic

Create composables for reusable logic:

// Don't repeat yourself - create composables
const { user, login, logout } = useAuth()
const { theme, toggleTheme } = useTheme()
const { todos, addTodo, removeTodo } = useTodos()

3. Prefer ref for Primitives

Use ref for primitive values and reactive for objects:

// Good
const count = ref(0)
const user = reactive({ name: 'John', age: 30 })

// Avoid
const count = reactive({ value: 0 })
const user = ref({ name: 'John', age: 30 })

Conclusion

The Composition API represents a paradigm shift in how we write Vue applications. It provides better code organization, improved reusability, and superior TypeScript support. While there's a learning curve, the benefits in terms of maintainability and developer experience make it worthwhile.

Start by converting smaller components and gradually adopt the Composition API throughout your application. Remember, you can use both APIs in the same project, making migration gradual and manageable.

Resources

Kyler Johnson

Senior Principal Software Engineer building next-gen cybersecurity tools, web geek, blogger, and Linux enthusiast.

© 2025 Kyler Johnson. All rights reserved.