Vue 3's Composition API solves the Options API's logic-scatter problem in large components. It's a functional, composable and TypeScript-friendly paradigm similar to React hooks. This article teaches the Composition API from scratch with real-world examples.
<script setup> - The Modern Syntax
Related guides: PostgreSQL optimization · Advanced Git commands · What is Redis · Deploying with Docker · Docker Compose guide · What Is Software Development? Processes, Methodologies, and Tools
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} - ${newVal}`);
});
onMounted(() => {
console.log('Component mounted');
});
</script>
<template>
<button @click="increment">{{ count }} × 2 = {{ doubled }}</button>
</template>ref vs reactive
Vue offers two APIs for reactive state:
ref()- for primitive values (number, string, boolean). Accessed via.valuereactive()- for objects/arrays. Direct access, but destructuring breaks reactivity
import { ref, reactive, toRefs } from 'vue';
// ref - primitive
const count = ref(0);
count.value++; // .value is required in JS
// In the template {{ count }} - auto-unwrapped
// reactive - object
const state = reactive({
name: 'Alex',
age: 30
});
state.age++; // direct access
// WRONG - destructuring breaks reactivity
const { age } = state; // age is no longer reactive
// RIGHT - use toRefs
const { age: ageRef } = toRefs(state);
ageRef.value++; // worksComputed
const firstName = ref('Alex');
const lastName = ref('Johnson');
// Readonly computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// Writable computed (getter + setter)
const fullNameRW = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (v) => {
[firstName.value, lastName.value] = v.split(' ');
}
});Watch and WatchEffect
import { ref, watch, watchEffect } from 'vue';
const userId = ref(1);
const user = ref(null);
// watch - explicit source, runs when it changes
watch(userId, async (newId) => {
user.value = await fetchUser(newId);
}, { immediate: true }); // also runs on initial load
// Multiple sources
watch([userId, other], ([newId, newOther]) => {});
// Deep watch
watch(stateObject, fn, { deep: true });
// watchEffect - auto-tracks every reactive it uses
watchEffect(() => {
console.log(`user ${userId.value} = ${user.value?.name}`);
});Lifecycle Hooks
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured
} from 'vue';
onMounted(() => console.log('mounted'));
onUnmounted(() => console.log('cleanup'));Composables - Reusable Logic
A composable is a reusable function built on the Composition API - the Vue counterpart of a React custom hook. Convention: the use prefix.
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
watchEffect(async () => {
const u = toValue(url); // unwrap a ref or getter
data.value = null;
error.value = null;
loading.value = true;
try {
const res = await fetch(u);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}<script setup>
import { ref } from 'vue';
import { useFetch } from './composables/useFetch';
const userId = ref(1);
const url = computed(() => `/api/users/${userId.value}`);
const { data: user, loading, error } = useFetch(url);
</script>
<template>
<p v-if="loading">Loading...</p>
<p v-else-if="error">{{ error.message }}</p>
<pre v-else>{{ user }}</pre>
</template>Props and Emits
<script setup>
const props = defineProps({
id: { type: Number, required: true },
label: { type: String, default: ' }
});
const emit = defineEmits(['update', 'delete']);
function save() {
emit('update', { id: props.id, changed: true });
}
</script>
<!-- Parent -->
<UserCard :id="1" label="Alex" @update="handleUpdate" />v-model with Composition
<!-- Parent -->
<CustomInput v-model="text" />
<!-- CustomInput.vue (Vue 3.4+) -->
<script setup>
const model = defineModel(); // two-way binding
</script>
<template>
<input v-model="model" />
</template>Pinia (Store)
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const isAdmin = computed(() => user.value?.role === 'admin');
async function login(email, password) {
user.value = await api.login(email, password);
}
function logout() {
user.value = null;
}
return { user, isAdmin, login, logout };
});
// In a component
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login(email, password);Options API vs Composition API
- Options API: easier for beginners, enforces a tidy component structure
- Composition API: logic can be grouped, shared through composables, TypeScript is more comfortable
- Team decision: don't mix them - a component is either Options or Composition
Modern Software Development and DevOps Practices
Professional software development rests on three pillars: version control (Git + GitHub/GitLab pull request flow, mandatory code review), CI/CD pipeline (automated test + lint + build + deploy), and observability (Sentry/Datadog/Grafana for logs, metrics, traces). The test pyramid (unit > integration > e2e) ensures code quality, microservice architecture uses Docker containers with Kubernetes orchestration, and REST or GraphQL APIs follow OpenAPI/GraphQL Schema contracts. Across the SDLC (requirements - design - implementation - test - deploy - maintenance), Agile/Scrum sprints last 1-2 weeks while DevOps teams practice continuous delivery.