βοΈ Composition API Fundamentals
The Composition API is Vue 3's flexible, function-based way to write components. Instead of scattering a feature across data, methods, and computed buckets, you keep everything about a feature together β and you can lift that logic straight out into a reusable function. This lesson covers reactivity, derived state, watchers, lifecycle, template refs, and composables.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Create reactive state with
refandreactiveand explain when to reach for each - Derive state with
computed, including writable computed properties - Run side effects with
watchandwatchEffect - Use Composition-API lifecycle hooks and template refs to access the DOM
- Extract and reuse stateful logic by writing your own composables
Estimated Time: 45β55 minutes β’ Difficulty: Intermediate
Hands-on: Write a useCounter composable and use it in a component.
In This Lesson
Why the Composition API?
The Composition API is a set of functions β ref, computed, watch, the on* hooks β that you call to build a component's logic. It exists to solve a real pain in the older Options API: as a component grows, the code for one feature gets smeared across separate data, methods, computed, and watch sections, forcing you to scroll around to understand a single behavior.
π‘ A useful analogy: The Options API sorts your belongings by type β all cables in one drawer, all chargers in another, all manuals in a third. The Composition API sorts by purpose β everything for your camera in one box, everything for your laptop in another. When you need the camera, you grab one box instead of raiding three drawers.
The Options API organizes by option type; the Composition API keeps each feature's code together.
π‘ Not a replacement
The Composition API doesn't kill the Options API β both are fully supported in Vue 3. But for new code, especially anything non-trivial, the Composition API with <script setup> is the recommended default, and it's what this course uses throughout.
script setup
The <script setup> block is the modern, compile-time-optimized entry point for the Composition API. Every top-level binding you declare β variables, functions, imported components β is automatically available in the template. No return statement, no boilerplate.
<template>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('mounted'))
// Everything above is auto-exposed to the template
</script>
π The older setup() function
You may see the Composition API written as a setup() function inside a normal export default, which must explicitly return what the template uses. It works, but <script setup> is more concise, faster at runtime, and better for tooling β prefer it for new components.
ref and reactive
Reactivity is the engine that re-renders your UI when data changes. The Composition API gives you two ways to create reactive state.
ref() β a reactive box for any value
ref() wraps a value in a reactive container with a single .value property. It works for primitives and objects. You read and write .value in JavaScript, but in templates Vue unwraps it for you automatically.
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++ // reactive update
console.log(count.value) // 1
const user = ref({ name: 'Ada', age: 30 })
user.value.age = 31 // still reactive
π‘ What is aref, really? Think of a museum display case. The case itself stays put, but when you swap the artifact inside, sensors notice and update the information panel. Arefis that case:.valueis the artifact, and Vue is the sensor that refreshes the UI whenever it changes.
β οΈ The .value gotcha
In <script> you must use .value to read or write a ref. In the <template> you must not β {{ count }}, not {{ count.value }}. Forgetting .value in JavaScript is the single most common beginner mistake.
reactive() β a deeply reactive object
reactive() makes an entire object reactive with no .value needed. It only works on objects, arrays, and collections β not primitives.
import { reactive } from 'vue'
const state = reactive({
name: 'Ada',
address: { city: 'London' }
})
state.name = 'Grace' // reactive
state.address.city = 'New York' // nested changes are reactive too
Which should I use?
Reach for ref when⦠| Reach for reactive when⦠|
|---|---|
| Working with a primitive (string, number, boolean) | Grouping several related fields into one object |
| You may reassign the whole value | You only mutate properties, never replace the object |
| Passing a reactive value into a function or composable | You want natural obj.prop syntax without .value |
β A common convention
Many teams use ref for almost everything, primitives and objects alike, for consistency and to sidestep reactive's pitfalls. That's a perfectly good default.
β οΈ Don't destructure a reactive object
Destructuring pulls values out of the reactive proxy and breaks the connection:
const state = reactive({ name: 'Ada', age: 30 })
let { name } = state // β 'name' is now a plain, non-reactive string
Use toRefs to keep each property reactive when you destructure:
import { reactive, toRefs } from 'vue'
const state = reactive({ name: 'Ada', age: 30 })
const { name, age } = toRefs(state)
name.value = 'Grace' // β
updates state.name
Computed Properties
computed() creates derived state β a value calculated from other reactive values that updates automatically and caches its result until a dependency changes.
import { ref, computed } from 'vue'
const firstName = ref('Grace')
const lastName = ref('Hopper')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
console.log(fullName.value) // 'Grace Hopper'
firstName.value = 'Ada'
console.log(fullName.value) // 'Ada Hopper' β recalculated automatically
π‘ Computed vs. method
A computed caches: it only re-runs when a dependency changes, so reading it repeatedly is cheap. A method re-runs on every single render. Use computed for derived values, methods for actions triggered by events.
Writable computed
Pass an object with get and set to make a computed writable β handy when a single derived value maps back onto several sources:
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
const parts = value.split(' ')
firstName.value = parts[0]
lastName.value = parts[parts.length - 1]
}
})
fullName.value = 'Katherine Johnson'
console.log(firstName.value) // 'Katherine'
console.log(lastName.value) // 'Johnson'
Watchers
Where computed produces a value, a watcher runs a side effect β fetching data, writing to localStorage, logging β in response to changes.
watch: react to a specific source
import { ref, watch } from 'vue'
const query = ref('')
const results = ref([])
watch(query, async (newQuery, oldQuery) => {
if (!newQuery.trim()) {
results.value = []
return
}
const res = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`)
results.value = await res.json()
})
You can watch multiple sources at once, watch deeply for nested changes, or run immediately on setup:
// Watch several sources
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`Name is now ${newFirst} ${newLast}`)
})
// Deep watch a reactive object + run once immediately
watch(user, (value) => save(value), { deep: true, immediate: true })
watchEffect: track dependencies automatically
watchEffect runs immediately and re-runs whenever any reactive value it used changes β you never list dependencies explicitly.
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
watchEffect(async () => {
// userId.value is read here, so this re-runs whenever it changes
const res = await fetch(`/api/users/${userId.value}`)
userData.value = await res.json()
})
userId.value = 2 // triggers the effect again
π watch vs. watchEffect
Use watch when you need the old and new values, or want the effect to run only when a specific source changes. Use watchEffect for effects that depend on several values and should simply stay in sync. Both return a stop function β call it to cancel the watcher early.
Lifecycle & Template Refs
The Composition API exposes lifecycle hooks as imported on* functions. There's no onCreated β the top level of <script setup> is the creation phase.
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
// Code here runs during creation (replaces beforeCreate/created)
onMounted(() => {
console.log('DOM is ready')
})
onBeforeUnmount(() => {
console.log('cleaning up before removal')
})
</script>
Template refs
To reach an actual DOM element, declare a ref whose name matches the element's ref attribute. It's null until the component mounts, so access it inside onMounted.
<template>
<input ref="inputEl" />
<button @click="focusInput">Focus</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputEl = ref(null)
onMounted(() => {
inputEl.value.focus() // the element exists now
})
function focusInput() {
inputEl.value.focus()
}
</script>
π‘ Why null at first?
The ref only gets connected to the real element after the template renders. Reading it at the top of <script setup> gives null; reading it in onMounted (or an event handler that fires later) gives the element.
Composables: Reusing Logic
The headline payoff of the Composition API is the composable: a plain function that uses Composition-API primitives to package stateful logic so you can reuse it across components. By convention, composables are named useSomething.
π‘ A useful analogy: Composables are like kitchen appliances. A blender does one job well and shows up in many recipes. Extract "counting" or "fetching" into a composable once, and every component can plug it in without re-implementing it.
Writing a composable
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initial = 0, step = 1) {
const count = ref(initial)
const doubled = computed(() => count.value * 2)
function increment() { count.value += step }
function decrement() { count.value -= step }
function reset() { count.value = initial }
return { count, doubled, increment, decrement, reset }
}
Using it in a component
<template>
<p>Count: {{ count }} (doubled: {{ doubled }})</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</template>
<script setup>
import { useCounter } from './composables/useCounter'
const { count, doubled, increment, decrement, reset } = useCounter(10, 2)
</script>
A more useful example: useFetch
// composables/useFetch.js
import { ref, watchEffect, toValue, isRef } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function run() {
loading.value = true
error.value = null
try {
const res = await fetch(toValue(url)) // accepts a ref OR a string
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// If url is reactive, re-fetch when it changes; otherwise fetch once
if (isRef(url)) watchEffect(run)
else run()
return { data, error, loading, refresh: run }
}
β Composables vs. Vue 2 mixins
Composables fix the classic problems of mixins: the source of every value is an explicit import, naming collisions vanish because you rename on destructure, and dependencies are clear. They're the standard way to share logic in Vue 3. (The community library VueUse ships hundreds of ready-made ones.)
Hands-on Exercise
ποΈ Write and Use a useToggle Composable
Objective: Practice extracting reusable stateful logic β the heart of the Composition API.
Requirements:
- Create
composables/useToggle.jsexporting auseToggle(initial = false)function. - It should hold a boolean
refand exposetoggle(), plus explicitsetOn()andsetOff()helpers. - Return the state and the three functions.
- In a component, use it to show/hide a details panel with a single button.
- Prove reuse: call
useTogglea second time in the same component for an unrelated switch (e.g. dark mode) without any logic duplication.
π‘ Hint
Because a composable returns fresh state on each call, invoking useToggle() twice gives you two independent booleans. Rename them on destructure: const { state: showDetails, toggle: toggleDetails } = useToggle().
β Solution
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initial = false) {
const state = ref(initial)
const toggle = () => { state.value = !state.value }
const setOn = () => { state.value = true }
const setOff = () => { state.value = false }
return { state, toggle, setOn, setOff }
}
<template>
<button @click="toggleDetails">
{{ showDetails ? 'Hide' : 'Show' }} details
</button>
<p v-if="showDetails">Here are the details!</p>
<button @click="toggleDark">Toggle dark mode</button>
<p>Dark mode is {{ dark ? 'on' : 'off' }}</p>
</template>
<script setup>
import { useToggle } from './composables/useToggle'
// Two independent instances β no duplicated logic
const { state: showDetails, toggle: toggleDetails } = useToggle()
const { state: dark, toggle: toggleDark } = useToggle(false)
</script>
Best Practices
| β Do | π« Don't |
|---|---|
Remember .value for refs in <script> | Write count.value in the template (use {{ count }}) |
Use computed for derived values | Recompute the same value in a method on every render |
Use toRefs when destructuring a reactive object | Destructure a reactive object directly (breaks reactivity) |
Extract shared logic into useX composables | Copy-paste the same ref/watch logic between components |
Access template refs inside onMounted | Read a template ref at the top of <script setup> (it's null) |
π― Quick Quiz
Question 1: Inside <script setup>, how do you read and update a ref called count?
Question 2: You need a value that stays in sync with two refs and re-computes only when they change. Which tool fits?
Question 3: What is the main advantage of extracting logic into a composable?
Summary & Quiz
π Key Takeaways
- The Composition API groups a feature's code together and makes logic reusable; write it in
<script setup>. refwraps any value (use.valuein JS);reactivemakes objects deeply reactive β don't destructure it withouttoRefs.computedgives cached derived state; watchers (watch,watchEffect) run side effects.- Lifecycle hooks are
on*functions; template refs reach the DOM and are ready inonMounted. - Composables (
useXfunctions) are the standard way to extract and share stateful logic.
π Further Reading
- Vue.js β Composition API FAQ
- Vue.js β Reactivity Fundamentals
- Vue.js β Composables
- VueUse β a large collection of ready-made composables
π What's Next?
You've now covered Vue's core. Next we pivot to the other framework in this module: Angular Framework Architecture β a look at how Angular structures an application with standalone components, signals, and dependency injection.
π Nice work!
You've got the Composition API in hand β the foundation of modern Vue. On to Angular.