What is Vue?
Vue is a progressive JavaScript framework for building user interfaces. The word is deliberate: you can drop Vue into a single page and enhance one widget, or use it with a build tool to build a full application. There is no rewrite required to get value, and the framework grows with your needs.
At its heart Vue is two things: a template syntax that extends HTML, and a reactivity system that tracks your data and updates the view automatically. You declare what the UI should show, and Vue keeps it in sync.
Single-file components
A Vue component usually lives in a .vue file with three blocks: a template, a script and styles. Keeping them together makes a component a single, self-contained unit.
<!-- Card.vue -->
<script setup>
defineProps({ title: String });
</script>
<template>
<article class="card">
<h3>{{ title }}</h3>
<slot />
</article>
</template>
<style scoped>
.card {
border-radius: 1rem;
padding: 1.5rem;
}
</style>
<style scoped> automatically limits the CSS to this component, so class names cannot leak out. <script setup> is a compiler macro that makes the Composition API concise and is the modern default.
Template syntax
Vue templates are HTML with a small set of directives. Interpolation uses double braces, and directives are attributes prefixed with v-.
<!-- profile.vue -->
<template>
<h1>{{ user.name }}</h1>
<p v-if="user.online">Online</p>
<p v-else>Offline</p>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
<img :src="user.avatar" :alt="user.name" />
<button @click="follow">Follow</button>
</template>
Read the shorthands as: :src binds an attribute to an expression, and @click listens for an event. v-if conditionally renders, and v-for loops with a :key for efficient updates. The template compiles to render functions, but you rarely need to see them.
Reactivity: ref and reactive
Vue’s reactivity system watches your data and re-renders when it changes. In the Composition API you create reactive values with ref or reactive.
<script setup>
import { ref, reactive } from "vue";
const count = ref(0); // .value in script
const user = reactive({ // proxy, no .value
name: "Ada",
online: true,
});
function increment() {
count.value += 1;
}
</script>
ref wraps any value and is accessed through .value in script, though templates unwrap it for you. reactive returns a deeply reactive proxy for an object. A common convention is to use ref consistently, since it works for primitives and is easy to reassign.
Computed and watch
Two functions handle derived values and side effects.
<script setup>
import { ref, computed, watch } from "vue";
const items = ref([10, 20, 30]);
const total = computed(() =>
items.value.reduce((sum, n) => sum + n, 0),
);
watch(items, (next, prev) => {
console.log("items changed", prev, next);
});
</script>
computed caches its result and recalculates only when a dependency changes — use it whenever a value is derived from other state. watch runs a callback when a source changes and is for side effects such as logging, fetching or saving. Reaching for watch to compute a value is a common mistake.
Props, emits and slots
Data flows down through props and up through emitted events.
<!-- Child.vue -->
<script setup>
const props = defineProps({
label: { type: String, required: true },
});
const emit = defineEmits(["select"]);
function handleClick() {
emit("select", props.label);
}
</script>
<template>
<button @click="handleClick">{{ label }}</button>
</template>
<!-- Parent.vue -->
<Child label="Save" @select="onSelect" />
Props are read-only in the child, which keeps the source of truth in the parent. Slots let a parent pass markup into a child, the equivalent of React’s children.
Composables
The Composition API’s biggest win is reusable logic. A composable is a function that uses reactivity and returns state, named with a use prefix.
// useFetch.js
import { ref, watchEffect } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
watchEffect(async () => {
data.value = null;
error.value = null;
try {
const res = await fetch(url.value);
data.value = await res.json();
} catch (err) {
error.value = err;
}
});
return { data, error };
}
Composables share logic, not state, so each component that calls useFetch gets its own independent data. This is the Vue equivalent of a custom hook.
Routing and state
Vue Router handles navigation with a declarative mapping from URL to component, and Pinia is the official store for shared state.
// router.js
import { createRouter, createWebHistory } from "vue-router";
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/users/:id", component: UserProfile },
],
});
Reach for a store when several distant components need the same state. For local and parent-child data, props and composables are usually enough.
Best practices
- Use
script setupand the Composition API for new code. - Prefer
computedoverwatchfor derived values. - Keep props read-only and emit events for changes.
- Extract reusable logic into composables.
- Use
scopedstyles, or CSS Modules, to keep styles local. - Name composables with a
useprefix. - Avoid reaching into child state; communicate through props and emits.
Common mistakes
- Forgetting
.valueon a ref inside script. - Using
watchto compute a value thatcomputedshould derive. - Mutating props in a child instead of emitting an event.
- Destructuring a
reactiveobject and losing reactivity. - Putting all state in a global store when local state would do.
- Mixing the Options API and Composition API without a clear reason.
Where to go next
Vue is a gentle entry into component frameworks and scales all the way up. Add Nuxt for server rendering and routing conventions, compare the approach with React and Svelte, and keep your JavaScript sharp. Then build something small — a filterable list or a form with validation — and let the reactivity do the work.