Template Syntax and Reactivity
Interpolation, directives like v-if and v-for, and reactive state with ref() and reactive().
Text interpolation
The most basic form of Vue's template syntax is "mustache" interpolation — double curly braces embed a JavaScript expression's value directly into text content:
<script setup>
import { ref } from "vue";
const name = ref("Ada");
const score = ref(97);
</script>
<template>
<p>Hello, {{ name }}! Your score is {{ score }}.</p>
<p>Next milestone: {{ score + 3 }}</p>
</template>
Like JSX's {}, {{ }} accepts any single JavaScript expression — arithmetic, ternaries, function calls — but not statements. Notice that score, a ref, is written without .value inside the template — Vue automatically unwraps top-level refs there, which is a template-only convenience.
Directives
A directive is a special v- prefixed attribute that applies reactive behavior to an element in the template.
v-bind — binding an attribute to an expression
<template>
<img v-bind:src="imageUrl" v-bind:alt="imageAlt" />
<!-- shorthand: the colon alone -->
<img :src="imageUrl" :alt="imageAlt" />
</template>
v-bind (almost always written with its : shorthand) ties an HTML attribute's value to a JavaScript expression, re-evaluating it whenever the expression's dependencies change.
v-on — listening for events
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button v-on:click="increment">Count: {{ count }}</button>
<!-- shorthand: the @ symbol -->
<button @click="increment">Count: {{ count }}</button>
<!-- inline expressions work too, for simple cases -->
<button @click="count++">Increment inline</button>
</template>
v-on (almost always written as @) attaches an event listener. It accepts either the name of a method defined in <script setup> or an inline expression.
v-if — conditional rendering
<script setup>
import { ref } from "vue";
const isLoggedIn = ref(false);
</script>
<template>
<p v-if="isLoggedIn">Welcome back!</p>
<p v-else>Please log in.</p>
</template>
v-if removes the element from the DOM entirely when its expression is falsy (not just hides it with CSS) — use v-show instead when you need to toggle visibility frequently and want to avoid the cost of repeatedly destroying and recreating the element.
v-for — rendering lists
<script setup>
import { ref } from "vue";
const fruits = ref(["Apple", "Banana", "Cherry"]);
</script>
<template>
<ul>
<li v-for="fruit in fruits" :key="fruit">
{{ fruit }}
</li>
</ul>
</template>
Just like React's list keys, Vue's :key gives each rendered item a stable identity across re-renders — always provide one, and prefer a stable unique ID over the array index whenever the list can reorder or have items inserted/removed.
Reactive state: ref() vs reactive()
Vue's Composition API gives you two primitives for declaring reactive state.
ref() wraps any value — primitive or object — in a reactive container, accessed via .value in script code:
import { ref } from "vue";
const count = ref(0);
const user = ref({ name: "Ada", age: 28 });
count.value++;
user.value.age++; // still works — ref() makes nested objects reactive too
reactive() makes an object's properties directly reactive, without a .value wrapper — but it only works on objects (not primitives like a plain number or string):
import { reactive } from "vue";
const user = reactive({ name: "Ada", age: 28 });
user.age++; // no .value needed
<script setup>
import { ref, reactive } from "vue";
const count = ref(0);
const user = reactive({ name: "Ada", age: 28 });
function birthday() {
user.age++;
}
</script>
<template>
<p>Count: {{ count }}</p>
<p>{{ user.name }} is {{ user.age }}</p>
<button @click="birthday">Have a birthday</button>
</template>
In practice, ref() is the more commonly reached-for default — it works uniformly for any value type, and its explicit .value in script code makes it clear when you're reading/writing reactive state versus a plain variable. reactive() is a good fit for a single cohesive object of related state, but has a sharper edge: destructuring its properties out (const { name } = user) loses reactivity, since you get a plain disconnected value rather than a live reference back to the reactive object.
Common mistakes
- Forgetting
.valuewhen reading or writing aref()from<script setup>JavaScript — only the template auto-unwraps it. - Destructuring a
reactive()object's properties into standalone variables, which silently disconnects them from reactivity. - Using
v-iffor something toggled very frequently (like a tab switch several times a second) wherev-show's CSS-only toggle would be cheaper —v-iffully creates/destroys the element and its component state each time. - Omitting
:keyon av-for, or using the array index as the key for a list that can reorder — leads to the same kind of stale-state-attached-to-the-wrong-row bugs seen in other frameworks' list rendering.