The Composition API: ref, reactive, setup

intermediate
15 min

The Composition API: ref, reactive, setup

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.

Options API vs Composition API

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.

js
// Options API (still supported) export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } }
vue
<!-- 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.

ref: The Universal Container

js
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 works

A 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()).

reactive: Proxied Objects

js
import { reactive } from 'vue' const state = reactive({ items: [], loading: false }) state.items.push('a') // reactive, no .value needed

Limitations to respect:

  • Works only on objects, arrays, Maps, Sets -- not primitives.
  • Cannot be replaced: state = reactive({...}) breaks every existing reference.
  • Loses reactivity when destructured: 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.

Composables: Reusable Stateful Logic

A composable is just a function that uses reactivity APIs and returns state. By convention it starts with use:

js
// 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:

vue
<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).

Key Takeaways

  • The Composition API groups code by feature, and <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.
  • Default to ref; use toRefs when destructuring reactive objects.
  • Composables (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.

The Composition API: ref, reactive, setup - Vue.js | CodeYourCraft | CodeYourCraft