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:

HTML
<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

HTML
<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

HTML
<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

HTML
<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

HTML
<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:

Javascript
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):

Javascript
import { reactive } from "vue";

const user = reactive({ name: "Ada", age: 28 });

user.age++; // no .value needed
HTML
<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 .value when reading or writing a ref() 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-if for something toggled very frequently (like a tab switch several times a second) where v-show's CSS-only toggle would be cheaper — v-if fully creates/destroys the element and its component state each time.
  • Omitting :key on a v-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.