Reactivity and template syntax
ref versus reactive, computed versus watch, and the template directives you will use in every component.
Declaring reactive state
import { ref, reactive, computed } from 'vue'
const count = ref(0) // primitive: read and write with .value
const user = reactive({ name: 'Ada', tags: [] }) // object: properties are tracked directly
const double = computed(() => count.value * 2) // cached derived value
count.value += 1
user.name = 'Grace' // no .value for reactive objectsrefholds any value and always needs.valuein script — but templates unwrap it automatically, so you write{{ count }}.reactiveonly works with objects and arrays, and it is shallow when destructured: pullingconst { name } = userout of it severs the reactive link.- Use
toRefs(user)when you genuinely need to destructure and stay reactive. computedcaches on its dependencies; a plain function inside the template re-runs on every render.
Template directives
<p>{{ user.name }}</p>
<img :src="avatarUrl" :alt="user.name + ' avatar'">
<button @click="count++" @keyup.enter="submit">Add</button>
<input v-model="user.name"> <!-- two-way binding -->
<p v-if="count > 0">positive</p>
<p v-else-if="count < 0">negative</p>
<p v-else>zero</p>
<ul>
<li v-for="tag in user.tags" :key="tag">{{ tag }}</li>
</ul>| Syntax | Long form | Purpose |
|---|---|---|
{{ x }} | — | Interpolate text (escaped, safe) |
:href | v-bind:href | Bind an attribute or prop |
@click | v-on:click | Listen to an event |
v-model | — | Two-way binding on inputs and components |
v-if / v-show | — | Remove from the DOM / toggle display |
v-for | — | Render a list; always pass :key |
Modifiers change a directive's behaviour: @submit.prevent stops the default action, @click.stop stops propagation, @click.once detaches after the first call, and v-model.lazy syncs on change instead of on every keystroke.
watch versus computed
import { ref, computed, watch, watchEffect } from 'vue'
const query = ref('')
const results = ref([])
const trimmed = computed(() => query.value.trim()) // derive
watch(trimmed, async (value, oldValue) => { // side effect
if (!value) { results.value = []; return }
results.value = await search(value)
})- computed: pure, cached, no side effects. If you find yourself assigning inside one, you wanted a
watch. - watch: runs a side effect after a source changes; accepts a ref, a getter, or an array of sources.
- watchEffect: runs immediately and re-runs whenever any reactive value it touched changes — convenient, but less explicit about what it tracks.
{ deep: true }is needed to react to mutations inside a nested object or array.- Clear timers and abort requests in the callback's cleanup function so a slow request cannot overwrite newer results.
watch(query, async (value, _old, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
results.value = await search(value, { signal: controller.signal })
})⚠️
Never mutate state inside a
computed, and never render v-html with untrusted input — it inserts raw HTML and is the standard XSS entry point in a Vue app.FAQ
ref or reactive?
Default to
ref: it works for primitives and objects alike, survives destructuring, and passes cleanly to composables. Reach for reactive when you have a cohesive record you never destructure.Why is my computed not updating?
It only tracks reactive sources read during evaluation. If it depends on something non-reactive — a plain variable, a DOM property, a date read once — it will not recompute.
Related
Components, props and events Arrays and iteration
Last refreshed 2026-09-18.