Back to Dev Notes
6 min read

Vue 3 Composition API: ref, reactive, and Beyond

Vue 3 Composition API: ref, reactive, and Beyond

The Composition API is Vue 3's answer to the question: "how do I reuse stateful logic without mixins?" After working with it on several production projects, I can confidently say it makes large applications significantly easier to reason about. Let's explore the key primitives.

Why the Composition API?

The Options API groups code by option type (data, methods, computed…). As components grow, related logic gets scattered. The Composition API lets you group code by feature, which keeps things cohesive.

ref vs reactive

Both create reactive state, but there are important differences:

import { ref, reactive } from 'vue'

// ref: wraps a primitive (access via .value)
const count = ref(0)
count.value++

// reactive: wraps an object (no .value needed)
const user = reactive({ name: 'Didik', role: 'dev' })
user.name = 'Budi'

A useful rule of thumb: use ref for single values and reactive for objects with multiple related properties.

computed Properties

import { ref, computed } from 'vue'

const firstName = ref('Didik')
const lastName  = ref('Prabowo')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// "Didik Prabowo"

Computed properties are cached based on their reactive dependencies, so they only re-evaluate when firstName or lastName changes.

watch & watchEffect

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

const query = ref(''  )

// watch: explicit source, runs on change
watch(query, (newVal, oldVal) => {
  console.log(`Search changed from "${oldVal}" to "${newVal}"`)
}, { immediate: true })

// watchEffect: auto-tracks dependencies
watchEffect(() => {
  console.log('Current query:', query.value)
})

Building a Custom Composable

This is where the Composition API really shines. Here's a reusable useFetch composable:

// composables/useFetch.ts
import { ref } from 'vue'

export function useFetch<T>(url: string) {
  const data    = ref<T | null>(null)
  const loading = ref(true)
  const error   = ref<string | null>(null)

  fetch(url)
    .then((res) => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json()
    })
    .then((json) => { data.value = json })
    .catch((err) => { error.value = err.message })
    .finally(() => { loading.value = false })

  return { data, loading, error }
}

// In any component:
const { data: users, loading } = useFetch<User[]>('/api/users')

Lifecycle Hooks

import { onMounted, onUnmounted, onBeforeMount } from 'vue'

onBeforeMount(() => console.log('Before mount'))
onMounted(()     => console.log('Mounted'))
onUnmounted(()   => console.log('Unmounted — clean up here!'))

Key Takeaways

  • Use ref for primitives, reactive for objects.
  • Computed values are lazy and cached — prefer them over methods for derived state.
  • watch is explicit; watchEffect is automatic.
  • Extract logic into composables to keep components lean and testable.

The Composition API might feel unfamiliar at first, but once it clicks you will never want to go back.

Tags
Back to Dev Notes

Are You Ready to kickstart your project with a touch of magic?

Reach out and let's make it happen ✨. I'm also available for full-time or part-time opportunities to push the boundaries of design and deliver exceptional work.

LaravelNuxt.jsVue.jsTypeScriptNext.jsPrismaTailwindBootstrapSuSupabase
PostgreSQLERPNextExpress.jsLinuxMySQLMongoDBRedisDockerGitHub