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
// 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 `.value` inside 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 `ref` for counters, booleans, or primitives that are independently updated.
- Use `reactive` for structured state like forms or nested objects (but be mindful of reactivity caveats with `Object.freeze` or direct property replacement).
- Use `computed` for 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:
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:
<!-- 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
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 setup` and 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`/`inject` or a store (Pinia) where appropriate.
- **Not cleaning up effects**: forget to unsubscribe in `onUnmounted` or to cancel async tasks, which can lead to memory leaks.
- **Mutating props directly**: props are read-only β use events or `v-model` to 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 `ref` for primitives and `reactive` for structured state**; use `computed` for derived values.
- **Type your props** and use `prop` validators for early errors.
- **Use Pinia for global state** rather than abusing `provide`/`inject` for 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](https://vuejs.org/)
- Composition API guide: [Composition API (Vue docs)](https://vuejs.org/guide/extras/composition-api-faq.html)
- Vue Mastery / Vue School (video courses)
- Pinia (state management): [pinia.vuejs.org](https://pinia.vuejs.org/)
- Nuxt (meta-framework): [nuxt.com](https://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](https://vuejs.org/)
- Composition API guide: [Vue Composition API](https://vuejs.org/guide/extras/composition-api-faq.html)
- Deep dive: Evan You & Vue core team blog posts
## Resources
- [MDN Web Docs](https://developer.mozilla.org/)
- [Web.dev](https://web.dev/)
- [Can I Use](https://caniuse.com/)
Comments