React
Vue 3 Composition API: ref, computed and composables
The Composition API moves state and logic into setup(): three primitives (ref, computed and composables) cover most Vue 3 components.
Key points
- setup() is the entry point: the template only sees what the function returns.
- A composable is a use* function returning refs: it is not a store and shares no state.
- Destructuring a reactive object breaks reactivity; refs survive because they are objects.
The Vue 3 Composition API moves state and logic into setup(): the same features as options, written as functions rather than as fields on an object. It is not a second framework, it is the same component instance with a different entry point.
Three primitives and one entry point
ref() creates a reactive box for a single value: in the script you read and write .value, while in the template Vue unwraps it for you. reactive() does the same for an object, but loses reactivity when you destructure it. computed() derives a value and recalculates it only when a dependency changes.
npm create vue@latest
The official generator offers TypeScript, Vue Router and Pinia: take TypeScript, and leave Pinia until state has to cross routes. Components start with <script setup>, the compact form of the Composition API.
import { computed, ref } from "vue";
const price = ref(0);
const qty = ref(1);
const total = computed(() => price.value * qty.value);
Total: {{ total.toFixed(2) }}
A composable is a function, not a store
A composable is a function that calls other primitives and returns refs: you import it, call it inside setup(), and it creates fresh state on every call. The use prefix is a convention, not a mechanism. In the example the cleanup aborts the previous request when the id changes or the component unmounts.
// composables/useUser.ts
import { ref, watch } from "vue";
export function useUser(id: () => string) {
const user = ref(null);
const loading = ref(false);
watch(
id,
async (value, _old, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
loading.value = true;
const res = await fetch("/api/users/" + value, { signal: controller.signal });
user.value = await res.json();
loading.value = false;
},
{ immediate: true },
);
return { user, loading };
}
Where it pays off and where it does not
| Case | Winner | Why |
|---|---|---|
| Logic reused in three components | Composition API | One composable replaces three mixins |
| A component made of props only | Options API | There is no logic to extract |
| State shared across routes | Neither | You need Pinia, not a composable |
| Testing logic on its own | Composition API | The function imports without mounting the component |
Mistakes that break reactivity
- Destructuring props or a
reactiveobject: the value is copied and stops updating. - Calling a composable outside
setup(): lifecycle hooks have no instance to attach to. - Writing
.valuein the template: Vue already unwraps it, and unwrapping twice returns nothing. - Computing a derived value inside
watchEffect:computedis declarative and cheaper.
The limitation to plan for
The Composition API does not decide where state lives. A composable called in two components creates two independent states: if you expect a shared value, that is a design bug, not a framework bug. State that crosses routes needs a Pinia store.
Catalogue components are already written with this API: shadcn-vue, vue-bits and PrimeVue install as source into your repository. The comparison with React is in Vue vs React; the library choice in UI libraries for Vue 3.
Frequently asked questions
Do I have to rewrite existing components?
No. The two APIs coexist: an options component can call a composable, and a setup component can use a mixin. Rewrite when you have to change the logic, not for aesthetic consistency.
ref or reactive?
ref for primitives and for objects you pass around, reactive for a group of fields that stay together. If you destructure often, ref costs less.
Why does the value stop updating after destructuring?
Because you copy the current value instead of keeping the reference. With a reactive object you need toRefs; with a ref the value field stays connected.
Can a composable replace Pinia?
No. A composable called in two places creates two separate states: fine for reused logic, wrong for a shared cart. That needs a store with a single instance.
Is the Composition API slower?
No. Rendering costs the same, what changes is how you declare state and dependencies. The practical difference is that a badly written composable creates more refs than needed.