Vue 3 Composition API Reference: ref, computed, Composables, Props, Pinia & Vue Router
Vue 3 Composition API is the recommended way to write Vue 3 components. It replaces the Options API’s scattered data/methods/computed/watch with collocated ref, computed, and watch calls — and makes it straightforward to extract reusable logic into composables.
1. ref, reactive, computed and watch
Core reactivity primitives and when to use ref vs reactive
<script setup lang="ts">
import { ref, reactive, computed, watch, watchEffect } from "vue";
// ref — reactive primitive (access with .value in script):
const count = ref(0);
const name = ref("Alice");
count.value++; // mutation in script
// In template: {{ count }} (no .value needed)
// reactive — reactive object (no .value needed):
const user = reactive({ name: "Alice", age: 30 });
user.name = "Bob"; // direct mutation
// Gotcha: don't destructure reactive — it loses reactivity:
// const { name } = user; BAD — name is no longer reactive
// Use toRefs to destructure safely:
// const { name, age } = toRefs(user);
// ref vs reactive:
// ref for primitives (string, number, boolean) and single values
// reactive for objects where you want to mutate properties directly
// Both work for objects — pick one and be consistent
// computed (derived state — auto-recalculates):
const doubled = computed(() => count.value * 2);
const fullName = computed(() => `${user.name} (${user.age})`);
// computed.value is readonly by default
// Writable computed:
const fullNameEditable = computed({
get: () => `${user.firstName} ${user.lastName}`,
set: (value) => {
[user.firstName, user.lastName] = value.split(" ");
}
});
// watch (run side effects when reactive data changes):
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`);
});
watch(() => user.name, (newName) => console.log("name changed:", newName));
watch([count, () => user.name], ([newCount, newName]) => {
// watch multiple sources
});
// watchEffect (auto-tracks dependencies — runs immediately):
watchEffect(() => {
console.log("count is", count.value); // re-runs when count changes
document.title = `Count: ${count.value}`;
});
</script>
2. Template Syntax & Directives
v-if, v-for, v-model, v-bind, v-on and the key directive
<template>
<!-- Text interpolation: -->
<p>{{ message }}</p>
<!-- v-bind (dynamic attributes): -->
<img :src="imageUrl" :alt="imageAlt" />
<button :disabled="isLoading" :class="{ active: isActive, error: hasError }">Save</button>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
<!-- v-on (event handling): -->
<button @click="handleClick">Click</button>
<button @click="count++">Inline</button>
<input @keydown.enter="submitForm" @keydown.esc="cancelForm" />
<!-- v-model (two-way binding): -->
<input v-model="searchQuery" />
<input v-model.trim="name" v-model.number="age" /> <!-- modifiers -->
<input type="checkbox" v-model="isChecked" />
<select v-model="selectedOption">
<option value="a">Option A</option>
</select>
<!-- v-if / v-else-if / v-else: -->
<div v-if="status === 'loading'">Loading...</div>
<div v-else-if="status === 'error'">Error!</div>
<div v-else>{{ data }}</div>
<!-- v-show (CSS display vs DOM removal): -->
<div v-show="isVisible">Toggled with CSS</div>
<!-- v-for (always use :key): -->
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
<!-- v-for with index: -->
<div v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</div>
</template>
3. Composables — Reusable Logic
Extract stateful logic into composable functions (Vue’s equivalent of React hooks)
// composables/useFetch.ts — reusable data fetching:
import { ref, watch } from "vue";
export function useFetch<T>(url: string | (() => string)) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const isLoading = ref(false);
async function fetchData() {
isLoading.value = true;
error.value = null;
try {
const res = await fetch(typeof url === "function" ? url() : url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
isLoading.value = false;
}
}
watch(() => (typeof url === "function" ? url() : url), fetchData, { immediate: true });
return { data, error, isLoading, refetch: fetchData };
}
// composables/useLocalStorage.ts:
export function useLocalStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key);
const value = ref<T>(stored ? JSON.parse(stored) : defaultValue);
watch(value, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal));
}, { deep: true });
return value;
}
// Usage in a component:
<script setup lang="ts">
import { useFetch } from "@/composables/useFetch";
import { useLocalStorage } from "@/composables/useLocalStorage";
const { data: users, isLoading, error } = useFetch<User[]>("/api/users");
const theme = useLocalStorage("theme", "light");
</script>
4. Props, Emits & Component Patterns
defineProps, defineEmits, defineExpose, provide/inject, and slots
// Child component:
<script setup lang="ts">
const props = defineProps<{
title: string;
count: number;
items?: string[]; // optional
}>();
// With defaults:
const props2 = withDefaults(defineProps<{ size?: "sm" | "md" | "lg" }>(), {
size: "md"
});
const emit = defineEmits<{
update: [value: string]; // typed event with payload
close: []; // event with no payload
}>();
// Emit an event:
function handleSubmit(value: string) {
emit("update", value);
}
// Expose methods to parent (for template refs):
defineExpose({ focus: () => inputRef.value?.focus() });
</script>
// Parent component:
<template>
<ChildComponent
:title="pageTitle"
:count="itemCount"
@update="handleUpdate"
@close="isOpen = false"
/>
</template>
// Provide/Inject (avoid prop drilling):
// In parent:
import { provide } from "vue";
provide("theme", ref("dark"));
provide("user", readonly(currentUser)); // readonly prevents child mutation
// In any descendant:
import { inject } from "vue";
const theme = inject("theme", ref("light")); // second arg = default
// Slot:
// Child:
<template>
<div class="card">
<slot name="header" :title="title" /> <!-- named slot with slot props -->
<slot /> <!-- default slot -->
</div>
</template>
// Parent:
<Card>
<template #header="{ title }"><h2>{{ title }}</h2></template>
<p>Card content</p>
</Card>
5. Vue Router & Pinia (State Management)
Route params, navigation guards, Pinia stores, and storeToRefs
// Vue Router 4:
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
// Route params and query:
const userId = route.params.id as string; // /users/:id
const page = route.query.page as string; // ?page=2
// Programmatic navigation:
router.push("/users");
router.push({ name: "user-detail", params: { id: "123" } });
router.push({ path: "/users", query: { page: "2" } });
router.replace("/login"); // replace history entry
router.back();
// Navigation guard (auth check):
router.beforeEach((to, from) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
// Route meta:
// { path: "/admin", component: Admin, meta: { requiresAuth: true, role: "admin" } }
// Pinia (state management):
import { defineStore } from "pinia";
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));
function addItem(item: CartItem) {
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.qty++;
else items.value.push({ ...item, qty: 1 });
}
function removeItem(id: string) {
items.value = items.value.filter(i => i.id !== id);
}
return { items, total, addItem, removeItem };
});
// In component:
import { storeToRefs } from "pinia";
const cart = useCartStore();
const { items, total } = storeToRefs(cart); // reactive refs from store
cart.addItem(product); // call action directly
Track Vue.js and frontend framework releases at ReleaseRun. Related: React Reference | TypeScript Reference | Next.js Reference
🔍 Free tool: npm Package Health Checker — check Vue.js packages — Vuex, Pinia, Vue Router — for known CVEs and active maintenance before upgrading.
Founded
2023 in London, UK
Contact
hello@releaserun.com