Vue 3 Composition API Without the Headache

matt
Matthew Gros · Jan 8, 2026

TLDR

Use script setup, ref() for single values, reactive() for objects, make composables for shared logic, stop overthinking it.

Vue 3 Composition API Without the Headache

The Composition API Seems Weird at First

I remember looking at it for the first time and thinking "why would I want this?" The Options API felt natural - data goes here, methods go there, computed properties over here.

Then I built something with more than 200 lines in a component and understood.

The Real Benefit

With Options API, related logic gets scattered. Your search feature has its data in data(), its methods in methods, its computed values in computed, and its watchers in watch. You're constantly jumping around.

Composition API lets you keep related stuff together.

Script Setup is the Way

Skip the verbose setup() function. Use <script setup>:

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

const count = ref(0)
const doubled = computed(() => count.value * 2)

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

<template>
    <button @click="increment">{{ count }} ({{ doubled }})</button>
</template>

Everything you declare is automatically available in the template. No more returning objects.

ref vs reactive

Here's the simple rule:

ref() for primitives (strings, numbers, booleans):

const name = ref('Matt')
const count = ref(0)
const isLoading = ref(false)

// Access with .value in script
name.value = 'New Name'

// Template unwraps automatically
// {{ name }} just works

reactive() for objects:

const user = reactive({
    name: 'Matt',
    email: 'matt@example.com'
})

// Direct access, no .value
user.name = 'Updated'

I mostly use ref() for everything now. It's more consistent and you don't have to think about whether something is reactive or a ref.

Computed Properties

Same concept as before, different syntax:

const items = ref([])
const completedCount = computed(() => {
    return items.value.filter(item => item.done).length
})

Computed values cache automatically - they only recalculate when their dependencies change.

Watching Things

import { watch, watchEffect } from 'vue'

// Watch specific value
watch(searchQuery, (newVal, oldVal) => {
    console.log('Search changed:', newVal)
    fetchResults(newVal)
})

// Watch multiple
watch([firstName, lastName], ([first, last]) => {
    fullName.value = `${first} ${last}`
})

watchEffect is useful when you want to track dependencies automatically:

watchEffect(() => {
    // This runs immediately and re-runs whenever userId changes
    fetchUser(userId.value)
})

Making Composables

This is where Composition API shines. Extract reusable logic:

// composables/useSearch.js
import { ref, watch } from 'vue'

export function useSearch(fetchFn) {
    const query = ref('')
    const results = ref([])
    const loading = ref(false)

    watch(query, async (value) => {
        if (!value) {
            results.value = []
            return
        }

        loading.value = true
        results.value = await fetchFn(value)
        loading.value = false
    }, { debounce: 300 })

    return { query, results, loading }
}

Use it anywhere:

<script setup>
import { useSearch } from '@/composables/useSearch'

const { query, results, loading } = useSearch(
    (q) => fetch(`/api/search?q=${q}`).then(r => r.json())
)
</script>

Props and Events

<script setup>
const props = defineProps({
    title: String,
    count: {
        type: Number,
        default: 0
    }
})

const emit = defineEmits(['update', 'delete'])

function save() {
    emit('update', { title: props.title })
}
</script>

Lifecycle Hooks

import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
    window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
    window.removeEventListener('resize', handleResize)
})

Stop Overthinking It

You don't need to refactor every component to use Composition API. Use it when:

  • Components get big and messy
  • You want to share logic between components
  • You're building something new

The Options API still works fine. Pick what makes your code clearer.

About the Author

matt

I build and ship automation-driven products using Laravel and modern frontend stacks (Vue/React), with a focus on scalability, measurable outcomes, and tight user experience. I’m based in Toronto, have 13+ years in PHP, and I also hold a pilot’s license. I enjoy working on new tech projects and generally exploring new technology.