Vue 3 brings a modern, composable approach to building UI with the Composition API, powerful reactivity primitives, and a simple component model. This guide is tailored for intermediate front-end engineers who want to move from concept to practice: clear examples, when to use which tool, and patterns that scale in real projects.
Core Terms & Abbreviations โ Quick Glossary ๐ค
- SFC (Single File Component):
.vuefiles that bundle template, script, and style in one place. - SPA (Single Page Application): an app that loads a single HTML page and updates dynamically on the client.
- SSR (Server-Side Rendering): rendering pages on the server for faster first paint and SEO benefits.
- SSG (Static Site Generation): build-time generation of HTML (e.g., using VitePress, Nuxt) for fast, cacheable pages.
- VDOM (Virtual DOM): an in-memory representation of the DOM used to batch updates efficiently.
- HMR (Hot Module Replacement): dev-time feature that swaps changed modules without a full reload.
- CDN (Content Delivery Network): edge servers that cache and serve assets for geographical speed.
If some of these terms are new, the later ‘Deployment & Architecture’ and resources sections contain links and examples.
Composition API โ Why it matters and how to use it ๐งฉ
The Composition API is a set of functions that lets you group logic by feature (instead of by option type like data/methods/computed). That improves reusability and organizes large components more naturally than the Options API.
Key ideas:
- Compose small, focused functions to encapsulate behavior (so-called “composables”).
- Keep state and side-effects local to the feature they belong to.
- Easier testing and reuse across components.
Example: extracting a useCounter composable
// composables/useCounter.js
import { ref } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
function increment() { count.value++ }
function reset() { count.value = initial }
return { count, increment, reset };
}
Using it in a component:
<script setup>
import { useCounter } from '@/composables/useCounter';
const { count, increment } = useCounter(0);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
### More Composition API tips
- Use small, focused composables (e.g., `useForm`, `useFetch`) to hide implementation details and make logic reusable across components.
- Prefer explicit return values from composables (avoid mutating global state inside composables unless that's the intent).
- Tests: composables are small functions and are straightforward to unit test.
Example: a `useFetch` composable with cancellation and loading state
```js
// composables/useFetch.js
import { ref } from 'vue';
export function useFetch(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
return { data, loading, error, fetchData };
}
Contrast with Options API: the Composition API avoids scattering related logic across data, methods, and computed, making it easier to extract and test.
Reactive Data โ ref, reactive, and computed ๐
Vue’s reactivity system is lightweight and intuitive. Choose the primitive that matches your use case:
ref(value): for a single reactive value (primitive or object). Access/set via.valueinside scripts.reactive(obj): for reactive objects with multiple properties (returns a proxied object).computed(() => ...): derived values that automatically update and are cached until dependencies change.
Examples:
import { ref, reactive, computed } from 'vue';
const name = ref('Ada');
const user = reactive({ id: 1, name: 'Ada', score: 10 });
const display = computed(() => `${user.name} (${user.score})`);
// update
name.value = 'Ada Lovelace';
user.score += 5;
When to use each:
- Use
reffor counters, booleans, or primitives that are independently updated. - Use
reactivefor structured state like forms or nested objects (but be mindful of reactivity caveats withObject.freezeor direct property replacement). - Use
computedfor derived state โ itโs efficient and expressive.
Tip: with ref and templates, Vue unwraps .value automatically, so you can {{ name }} in templates without .value.
Watching values: watch and watchEffect
Use watch when you want to react to specific reactive sources and run an effect only when they change. Use watchEffect for automatic tracking of dependencies (it reruns whenever tracked values change).
import { ref, watch, watchEffect } from 'vue';
const query = ref('');
watch(query, (newVal, oldVal) => {
// fetch when query changes (debounce in real code)
fetchData(newVal);
});
// watchEffect example (auto runs when any tracked reactive used inside changes)
watchEffect(() => {
console.log('User score is', user.score);
});
Note: watch supports immediate execution and cleanup return functions for async flows.
Component Patterns โ Props, Emits, Slots, and Reusability ๐งญ
A robust component architecture relies on clear data flow and composition. The core patterns are:
- Props: parent -> child data flow. Keep props small and explicitly typed where possible.
- Emits: child -> parent events. Always declare emits in the component so intent is clear.
- Slots: content distribution and composition; named and scoped slots unlock powerful patterns.
- Reusable components: favor small, focused components and compose them.
Props & Emits example:
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue']);
function onInput(e) { emit('update:modelValue', e.target.value) }
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
### v-model (shorthand for two-way bindings)
`v-model` is the standard way to create two-way bindings in Vue components. In `script setup` you can use `defineModel` or accept `modelValue` + `update:modelValue` emits manually (shown above). Example usage:
```vue
<!-- Parent.vue -->
<TextInput v-model="title" />
<!-- TextInput implements emits update:modelValue -->
Scoped slots (passing data from child to parent template)
Scoped slots let a parent provide rendering while the child passes data into the slot scope.
<!-- List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item">{{ item.text }}</slot>
</li>
</ul>
</template>
<!-- Usage -->
<List :items="todos">
<template #default="{ item }">
<strong>{{ item.text }}</strong>
</template>
</List>
Slots (simple and scoped):
<!-- Card.vue -->
<template>
<div class="card">
<header><slot name="header"/></header>
<main><slot/></main>
<footer><slot name="footer"/></footer>
</div>
</template>
<!-- Usage -->
<Card>
<template #header>
<h3>Title</h3>
</template>
Main content here
<template #footer>
<small>Footer</small>
</template>
</Card>
Reusable component strategies:
- Keep components small and focused on a single responsibility.
- Use composables for shared logic, not mixins (composables are explicit and testable).
- Prefer
v-modelandupdate:emits for two-way form integrations.
Lifecycle Hooks in Composition API โ What to use and when โฑ๏ธ
Vue 3 exposes lifecycle hooks as functions you call inside setup:
onBeforeMountโ just before mount (rarely needed).onMountedโ after the component is mounted; good for DOM-dependent code or initial network requests.onBeforeUpdateโ before a reactive update is applied.onUpdatedโ after an update is applied; useful for post-update reads.onBeforeUnmountโ just before unmount; prepare cleanup.onUnmountedโ after unmount; finalize cleanup.onErrorCapturedโ catch errors from child components (return false to prevent further propagation).
Examples & scenarios:
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
const id = setInterval(syncTime, 1000);
// cleanup when component unmounts
onUnmounted(() => clearInterval(id));
});
Another common pattern: subscribe/unsubscribe for external stores or web sockets
onMounted(() => subscribe(ws, handle));
onUnmounted(() => unsubscribe(ws, handle));
onErrorCaptured is useful when a parent wants to log or handle child errors without crashing the whole app:
onErrorCaptured((err, instance, info) => {
reportError(err, { component: instance.type.name, info });
return false; // prevent upward propagation
});
### When to use each lifecycle hook (practical notes)
- `onMounted`: fetch initial data, attach DOM listeners, start timers.
- `onBeforeUnmount` / `onUnmounted`: cleanup timers, cancel requests, remove event listeners.
- `onBeforeUpdate` / `onUpdated`: avoid heavy work in `onUpdated` that triggers additional updates; use it for post-update reads (e.g., re-measure layout).
- `onErrorCaptured`: log and optionally swallow errors from children; don't use it as a substitute for proper error handling.
Example: WebSocket subscribe/unsubscribe with cleanup
```js
import { onMounted, onUnmounted } from 'vue';
onMounted(() => subscribe(ws, handle));
onUnmounted(() => unsubscribe(ws, handle));
Conclusion & Next Steps โ
Vue 3’s Composition API and reactivity primitives unlock a clean, testable way to organize application logic. Use ref and reactive to model state, computed for derived values, and build components with clear props/emits/slots contracts. Lifecycle hooks let you manage side effects safely and predictably.
Try this next:
- Extract a composable from an existing component (e.g., form handling, data fetching).
- Replace a small Options API component with the Composition API and notice improved locality of logic.
- Read the Vue docs and experiment with
script setupand TypeScript for stronger developer ergonomics.
Deployment & Architecture Notes โ Text Graphs ๐๏ธ
Vue apps can be delivered in several ways depending on SEO and personalization needs. Here are simple text-graph illustrations:
Static SPA (SSG / S3 + CDN):
developer -> build -> static files -> CDN -> client (browser)
SSR (server-side rendering):
client -> CDN -> edge/server renderer -> backend API -> database
Image/Asset pipeline with optimization:
developer -> build -> CDN -> image optimizer (on-edge or third-party) -> client
Notes:
Notes:
- SSG/SSP (static) is simplest, fast to serve and cacheable; SSR helps with SEO and first-render performance for dynamic pages.
- Use CDN and image optimization for better global performance.
Common Pitfalls & Best Practices โ ๏ธโ
Pitfalls:
- Overusing global state: prefer localized composables and use
provide/injector a store (Pinia) where appropriate. - Not cleaning up effects: forget to unsubscribe in
onUnmountedor to cancel async tasks, which can lead to memory leaks. - Mutating props directly: props are read-only โ use events or
v-modelto update parent values. - Relying on object identity: when using
watch, remember that objects compared by reference may trigger extra runs; use deep watch only when necessary.
Best Practices:
- Compose with composables: extract reusable logic into small functions to ease testing and reuse.
- Prefer
reffor primitives andreactivefor structured state; usecomputedfor derived values. - Type your props and use
propvalidators for early errors. - Use Pinia for global state rather than abusing
provide/injectfor app-wide mutable state.
Pros, Cons & Alternatives โ Decision Guide โ๏ธ
Pros of Vue:
- Easy learning curve and excellent DX (developer experience).
- Clear conventions (SFCs,
v-model, directives) and first-class support for Composition API. - Great tooling (Vite, Vue DevTools) and solid ecosystem (Nuxt, Pinia, Vue Router).
Cons:
- Ecosystem is smaller than React’s; fewer enterprise-ready third-party libraries in some niches.
- Slightly different mental model compared to React; teams migrating from React may need adjustment.
Alternatives:
- React: larger ecosystem, mature patterns for complex apps; choose if you need broad library support and a huge talent pool.
- Svelte: compiles away reactivity with small runtime and excellent performance; choose Svelte for smaller bundles or simpler state models.
- Solid: fine-grained reactivity with great perf for highly dynamic UIs.
When to choose Vue:
- Ideal for teams that prefer clear conventions, approachable syntax, and strong single-file component ergonomics.
Further Reading & Resources ๐
- Official docs: vuejs.org
- Composition API guide: Composition API (Vue docs)
- Vue Mastery / Vue School (video courses)
- Pinia (state management): pinia.vuejs.org
- Nuxt (meta-framework): nuxt.com
- Evan You & Vue core blog posts for deep dives and RFCs
If you want, I can scaffold a small starter repo (Vite + Vue 3 + Pinia) with the composable examples and tests. Want me to create a starter/vue-composables/ directory and add a GitHub Actions preview workflow?
Further reading:
- Official Vue 3 docs: vuejs.org
- Composition API guide: Vue Composition API
- Deep dive: Evan You & Vue core team blog posts
Comments