Composition: The Composition API

Before the advent of Vue 3, developers relied primarily on the Options API to structure their components. As applications grew in scale, the limitations of this approach regarding logic organization and code reuse became increasingly apparent. The Composition API was created specifically to address these pain points. Built around the core philosophy of "composition," it allows developers to group code by logical concerns rather than arbitrary option types, making it effortless to split and reuse logic.

The Composition API is a set of function-based APIs introduced in Vue 3. It primarily encompasses:

  • Reactive APIs: ref, reactive, computed, watch, and more.
  • Lifecycle Hooks: onMounted, onUnmounted, onUpdated, and more.
  • Dependency Injection: provide and inject.
  • The Entry Point: setup() and its syntactic sugar <script setup>.

Unlike the Options API, which splits code across options like data, methods, and computed, the Composition API enables us to keep pieces of code related to the exact same feature right next to each other, drastically improving code locality and cohesion.
In complex components, the Options API typically suffers from several inherent drawbacks:

  1. Fragmented Logic: For instance, a single search feature might have its data defined in data, its handling methods in methods, and its side effects in watch. Developers are forced to constantly jump back and forth across different sections of a file to understand a single feature.
  2. Poor Reusability: While mixins allow logic sharing, they carry hidden risks such as namespace collisions and unclear property sources.
  3. Weak Type Inference: Code structures relying heavily on this context often lack robust support for TypeScript.

By leveraging plain functions to encapsulate logic, the Composition API naturally circumvents these challenges.

Core API Usage Examples

ref and reactive

ref is ideal for wrapping primitive types, accessed via .value, whereas reactive is well-suited for object types.

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

const count = ref(0)
const user = reactive({ name: 'John Doe', age: 25 })

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">Clicked {{ count }} times</button>
  <p>{{ user.name }}, {{ user.age }} years old</p>
</template>

computed and watch

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

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

// Computed property: automatically tracks dependencies
const fullName = computed(() => lastName.value + ' ' + firstName.value)

// Watcher: reacts to data changes
watch(firstName, (newVal, oldVal) => {
  console.log(`Name changed from ${oldVal} to ${newVal}`)
})

Modern Lifecycle Hooks

Lifecycle hooks in the Composition API are prefixed with on and registered directly inside the setup scope:

import { onMounted, onUnmounted } from 'vue'

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

onUnmounted(() => {
  console.log('Component has been unmounted')
})

Composables: The Best Practice for Logic Reuse

The greatest value of the Composition API lies in composition itself—extracting independent logic into reusable functions known as composables (conventionally named with a use prefix). For example, encapsulating mouse tracking logic:

// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

This can then be utilized seamlessly inside any component:

<script setup>
import { useMouse } from './useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position: {{ x }}, {{ y }}</p>
</template>

Compared to mixins, composables offer clear origins, prevent naming collisions, and provide native TypeScript type inference. Furthermore, multiple composables can call and nest within each other, building flexible layers of logic.

Best Practices

  • Prioritize the <script setup> syntactic sugar for cleaner code; it is also the officially recommended approach.
  • Use ref for primitive values and reactive for objects; when in doubt, sticking exclusively to ref reduces mental overhead.
  • Once component logic exceeds a certain complexity threshold, break it down into standalone composables to keep component files concise and readable.
  • Always clean up side effects like timers and event listeners inside onUnmounted to prevent memory leaks.

Conclusion

The essence of the Composition API is "composing logic through functions." It gathers related code, separates unrelated concerns, and offers a safer, clearer mechanism for reuse compared to mixins. For new projects, adopting the Composition API entirely is highly recommended. For legacy codebases, Vue 3 maintains full backward compatibility with the Options API, enabling a smooth and gradual migration. Mastering this suite of APIs will significantly elevate the maintainability and development efficiency of large-scale frontend applications.