You have been using pieces of the Composition API since lesson one. Now we step back and understand it as a system: why it exists, how ref and reactive differ, what <script setup> compiles to, and how composables let you extract and reuse stateful logic across components -- the feature that made the Composition API the standard for Vue 3.
Vue 2 organized components by option type -- all data together, all methods together. A feature like "search" ended up scattered across data, computed, methods, and watch. The Composition API organizes code by feature: everything about search sits together, everything about pagination sits together. In small components the difference is minor; in 300-line components it is transformative.
// Options API (still supported)
export default {
data() { return { count: 0 } },
methods: { increment() { this.count++ } }
}<!-- Composition API with <script setup> -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script><script setup> is compile-time sugar for a setup() function: every top-level binding is exposed to the template, imports work as components, and there is no this.
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: 'Ada' })
count.value++ // .value in JS
user.value.name = 'Grace' // deep reactivity works through refs too
user.value = { name: 'Linus' } // replacing the whole object also worksA ref wraps any value in a { value } container tracked by Vue. Its superpower over reactive is replaceability: you can assign a whole new value and reactivity survives, which matters constantly when fetching data (list.value = await fetchList()).
import { reactive } from 'vue'
const state = reactive({ items: [], loading: false })
state.items.push('a') // reactive, no .value neededLimitations to respect:
state = reactive({...}) breaks every existing reference.const { loading } = state yields a dead boolean. Use toRefs(state) if you must destructure.Because of these sharp edges, the official recommendation is simple: default to ref for everything, and reach for reactive when you want a tightly grouped object of state without .value noise.
A composable is just a function that uses reactivity APIs and returns state. By convention it starts with use:
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
fetch(url)
.then((r) => r.json())
.then((json) => (data.value = json))
.catch((e) => (error.value = e))
.finally(() => (loading.value = false))
return { data, error, loading }
}Any component can now write:
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, error, loading } = useFetch('https://api.example.com/posts')
</script>
<template>
<p v-if="loading">Loading…</p>
<p v-else-if="error">{{ error.message }}</p>
<ul v-else><li v-for="p in data" :key="p.id">{{ p.title }}</li></ul>
</template>Each call creates independent state -- two components using useFetch do not share data. Composables can call other composables, accept refs as arguments, and register lifecycle hooks. Libraries like VueUse offer hundreds of ready-made ones (useLocalStorage, useMouse, useDebounce).
<script setup> is its concise standard form.ref works for every value and survives reassignment; access with .value in scripts only.reactive offers .value-free objects but cannot be replaced or destructured safely.ref; use toRefs when destructuring reactive objects.useX functions) are the Vue-native way to share stateful logic without mixins.Next up: Lifecycle Hooks -- running code when components mount, update, and unmount.