Vue is a JavaScript framework for building user interfaces. If you have spent any time writing vanilla JavaScript, you have probably noticed that managing the DOM gets tedious as your app grows. Frameworks like Vue, React, and Svelte give you a way to describe what the UI should look like for a given state, and they handle the DOM updates for you. Vue is the friendliest of the bunch for beginners, while still being serious enough to power some of the largest apps in the world.
This article assumes you are comfortable with HTML, CSS, and modern JavaScript. If you need to brush up, our JavaScript for Beginners article is the place to start.
Why Vue, why now
Vue 3 (released in 2020, now the standard) is a complete rewrite that introduced the Composition API — a more flexible way to organise component logic. Compared to React, Vue has a shorter learning curve and a more opinionated template syntax. Compared to Svelte, Vue has a much larger ecosystem and a saner story for large apps. We use it for almost every front-end build at Mangobaz because it strikes the best balance between "feels easy" and "scales to a million lines."
Your first Vue component
The fastest way to try Vue is to load it from a CDN. Save this as hello.html:
<!doctype html>
<html>
<head><title>Hello</title></head>
<body>
<div></div>
<script src="https://unpkg.com/vue@3"></script>
<script>
const { createApp } = Vue;
createApp({
data() { return { name: "Mango" }; },
template: `<h1>Hello, {{ name }}!</h1>`,
}).mount("#app");
</script>
</body>
</html>
Open the file in a browser. You should see "Hello, Mango!" rendered inside the #app div. That is a complete Vue app — one component, one piece of reactive data, one template. From here, everything else is variations.
The Composition API: ref() and reactive()
In Vue 3, the recommended way to declare reactive state is with ref for primitives and reactive for objects. A ref wraps a value and lets Vue track when it changes:
import { ref } from "vue";
const count = ref(0);
console.log(count.value); // 0 (note: .value in script)
count.value = 5; // update
Inside the template, refs are auto-unwrapped — you write {{ count }}, not {{ count.value }}. That is a tiny ergonomic detail that makes templates feel natural.
For objects, use reactive:
import { reactive } from "vue";
const user = reactive({ name: "Mango", age: 20 });
user.age = 21; // tracked, no .value needed
The general rule of thumb in modern Vue: use ref unless you specifically need an object. We will follow that rule throughout this article.
Reactivity in action: a counter component
Let us build something a little more useful. Save this as counter.js:
import { ref } from "vue";
export default {
setup() {
const count = ref(0);
const inc = () => count.value++;
const reset = () => (count.value = 0);
return { count, inc, reset };
},
template: `
<div>
<h1>{{ count }}</h1>
<button @click="inc">+</button>
<button @click="reset">Reset</button>
</div>
`,
};
The setup() function is where you declare reactive state and functions. Whatever you return is available in the template. The @click shorthand means v-on:click. When count changes, Vue automatically re-renders the parts of the DOM that depend on it.
Single-file components (.vue files)
For real projects, you will not define templates inside JavaScript strings. Instead, you use Single-File Components — files with a .vue extension that bundle template, script, and style:
<template>
<h1>{{ count }}</h1>
<button @click="inc">+</button>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const inc = () => count.value++;
</script>
<style scoped>
button { font-size: 2rem; padding: 0.5rem 1rem; }
</style>
The <script setup> macro is the modern, concise way to write component logic — no setup() function, no explicit return. Everything declared at the top level is available to the template. The scoped attribute on <style> means the CSS only applies to this component, even if the class names collide elsewhere.
To use .vue files, you need a build tool — Vite is the standard. Vite compiles Vue, handles modules, and gives you a hot-reloading dev server in one command: npm create vue@latest.
Props and emits: passing data between components
Components communicate through props (parent to child) and emits (child to parent). Here is the pattern:
<!-- Parent.vue -->
<template>
<Greeting :name="userName" @greet="onGreet" />
</template>
<script setup>
import { ref } from "vue";
import Greeting from "./Greeting.vue";
const userName = ref("Mango");
const => console.log(msg);
</script>
<!-- Greeting.vue -->
<template>
<button @click="sayHi">Hi</button>
</template>
<script setup>
const props = defineProps({ name: String });
const emit = defineEmits(["greet"]);
const sayHi = () => emit("greet", `Hello, ${props.name}!`);
</script>
Props are declared with defineProps, emits with defineEmits. The compiler uses these to type-check and to enforce one-way data flow: props flow down, events flow up. Never mutate a prop inside the child — that breaks reactivity.
Computed values
Sometimes you want a value that depends on other reactive values. computed gives you a memoised value that updates only when its dependencies change:
import { ref, computed } from "vue";
const count = ref(0);
const doubled = computed(() => count.value * 2);
console.log(doubled.value); // 0
count.value = 5;
console.log(doubled.value); // 10
Use computed for any value that can be derived from other state. They are faster than recomputing in the template, and they make your templates much cleaner.
Lifecycle hooks
Every component has a lifecycle. Vue lets you hook into key moments with functions like onMounted, onUpdated, and onUnmounted:
import { onMounted, onUnmounted } from "vue";
onMounted(() => console.log("I just appeared"));
onUnmounted(() => console.log("I am being removed"));
The classic use case: fetch data when the component mounts, set up a subscription, clean it up when the component unmounts. Forgetting the cleanup is a classic source of memory leaks.
Common pitfalls
- Forgetting
.valuein script. In templates, refs are auto-unwrapped. In script, you must writecount.value. A common mistake is to destructure a ref and lose reactivity — usetoRefsto keep it. - Mutating props directly. Always emit an event instead. The parent's state should be the source of truth.
- Using the Options API in new code. It still works, but the Composition API is the recommended path for new projects.
- Skipping the build step. Single-file components need Vite. Trying to load
.vuefiles directly in the browser will not work. - Reaching for a state library too early. For most apps, plain reactive state is enough. Add Pinia only when you need to share state across many components.
Watchers: reacting to state changes
Sometimes you want to run a side effect when a piece of state changes — fetching data, saving to localStorage, debouncing. watch is the tool:
import { ref, watch } from "vue";
const name = ref("");
watch(name, (newValue, oldValue) => {
console.log(`Name changed from ${oldValue} to ${newValue}`);
});
By default, watch only fires when the value actually changes. If you want it to fire once immediately, pass { immediate: true }. If you want to debounce, pass { deep: true } for nested objects, or wrap the call in your own debounce function. Watchers are the imperative escape hatch for cases where computed values and templates are not enough.
Slots: reusable components with customisable content
Slots let a parent inject content into a child component. Think of them as placeholder areas the parent can fill:
<!-- Card.vue -->
<template>
<div>
<header>{{ title }}</header>
<slot>Default content</slot>
</div>
</template>
<!-- Parent.vue -->
<template>
<Card title="Welcome">
<p>This goes inside the card.</p>
</Card>
</template>
Further reading
Vue’s official documentation is widely regarded as one of the best in the frontend ecosystem. We send every beginner to it first, then to the Vue Router docs once they are ready to build a multi-page app.
- Vue.js official guide — the official Vue 3 guide — installation, the Options API, the Composition API, and reactivity in one place.
- Vue 3 Composition API FAQ — the official FAQ on the Composition API, covering when to use it, how it relates to the Options API, and the common migration questions.
- Vue Router documentation — the official documentation for Vue Router 4, covering route definitions, navigation guards, and lazy-loaded routes.
code>
The <slot> tag inside Card.vue is replaced with whatever the parent puts between <Card> and </Card>. Named slots (<slot name="footer">) give you multiple insertion points. Slots are how you build truly reusable components.
The ecosystem: Vite, Pinia, Vue Router
For a real app, you will want three companions to Vue:
- Vite — the build tool and dev server. Already used by default in
npm create vue@latest. - Pinia — the official state library. Replaces Vuex, much smaller, fully typed. Use it for app-wide state, not for component-local state.
- Vue Router — client-side routing for single-page apps. Most Vue apps need it once you have more than two views.
All three are official, well-maintained, and integrate with the Composition API cleanly. Install them only when you actually need them.
FAQ
Vue or React?
Either is fine. Vue has a shorter learning curve and a more opinionated template syntax. React is more popular and has more job openings in some markets. Pick Vue if you want to be productive quickly. Pick React if your local job market prefers it.
Do I need TypeScript?
No, but Vue's defineProps macro has excellent TypeScript support and will catch many bugs at compile time. Worth it once you are comfortable with the basics.
What is Vite?
Vite is a build tool. It compiles your .vue files into plain JavaScript the browser can run. It also gives you a hot-reloading dev server. It is the default for new Vue projects and a joy to use.
Should I learn the Options API or the Composition API?
Composition API for new code. Options API is still in lots of older tutorials and is not going away. Read both, write Composition. The official docs have moved fully to Composition since Vue 3.2.
How do I share state across components?
For two components, use props and emits. For deeply nested trees, use provide and inject. For an app-wide store, use Pinia (the official state library).
How do I add TypeScript to a Vue project?
When you scaffold with npm create vue@latest, the CLI asks if you want TypeScript. Say yes. Existing projects can add it by renaming .js files to .ts, adding vue-tsc, and configuring tsconfig.json. The setup takes about ten minutes and pays off forever.
Homework
Build a small to-do list app in Vue 3. It should have:
- A text input for new tasks and a button to add them.
- A list of tasks, each with a checkbox to mark complete and a delete button.
- A counter at the top showing "X tasks, Y completed".
- Use the Composition API throughout.
- Set it up with Vite:
npm create vue@latest.
Bonus: persist the tasks to localStorage so they survive a page refresh. That will teach you about watching reactive state with watch.