Vue, JavaScript, Tutorials Reactivity in Vue 3 — How Vue Makes Your UI Feel Alive
6 min read
3 viewsReactivity in Vue 3 — How Vue Makes Your UI Feel Alive
Part 3 of the Vue 3 Fundamentals series — understanding ref, reactive, computed, and watch.
If I had to pick the one thing that makes Vue feel magical, it’s reactivity.
Not the word — the concept. The fact that when your data changes, your UI just… updates. No DOM selectors. No innerHTML hacks. No manually calling a render function. Your data and your UI stay in sync automatically.
But here’s the thing: that magic isn’t random. It follows rules. And once you understand those rules, you stop guessing and start building with confidence.
That’s what this article is about.
What Is Reactivity, Really?
Think about a spreadsheet. When you change a value in cell A1, every formula that references A1 updates automatically. You don’t have to tell each formula “hey, go re-check your source.” The spreadsheet’s reactivity system handles that.
Vue’s reactivity system works the same way. When you declare a reactive variable and use it in your template, Vue tracks that connection. When the variable changes, Vue knows exactly which parts of the DOM depend on it — and updates only those parts.
This is fundamentally different from vanilla JavaScript, where you’d have to manually find DOM elements and update them every time data changes.
ref: Your Primary Reactivity Tool
The most common way to create reactive state in Vue 3 is ref:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello!')
const isVisible = ref(true)
</script>
ref wraps any value — a number, string, boolean, array, or object — in a reactive container. Vue can now watch that container for changes.
There’s one rule to remember: when you access or modify a ref inside JavaScript (not the template), you use .value:
console.log(count.value) // 0
count.value = 5 // updates to 5
count.value++ // increments to 6
Inside the template, Vue automatically unwraps the .value for you:
<p>{{ count }}</p> <!-- no .value needed here -->
<p>{{ message }}</p>
That’s the only footgun with ref. Remember .value in script, forget .value in template.
reactive: For Objects
When you’re working with a group of related values, reactive is a good fit:
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Nerando',
role: 'developer',
isLoggedIn: false
})
</script>
<template>
<p>{{ user.name }} — {{ user.role }}</p>
</template>
Unlike ref, you don’t need .value with reactive. You access properties like a normal JavaScript object:
user.name = 'Nerajno' // just works
user.isLoggedIn = true // just works
When should you use ref vs reactive? The Vue team’s current recommendation is to default to ref for almost everything. It’s more versatile — it handles primitives, objects, and arrays. You’ll find that consistent .value access actually becomes a helpful signal that “this thing is reactive.” Use reactive when you have a clearly grouped object of state and prefer the cleaner property access.
Computed Properties: Derived State
Sometimes you don’t need to store a value — you need to calculate it from existing reactive state. That’s what computed properties are for.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Nerando')
const lastName = ref('Johnson')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<template>
<p>{{ fullName }}</p>
</template>
fullName isn’t stored separately — it derives its value from firstName and lastName. Vue caches the result and only recalculates when one of those dependencies changes.
This is more efficient than a method (which would recalculate every render) and cleaner than storing the derived value manually. The rule of thumb: if a value can be calculated from existing state, make it a computed property.
watch: Reacting to Change
Sometimes you need to do something when a value changes — make an API call, log to analytics, update localStorage. That’s where watch comes in.
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
watch(searchQuery, (newValue, oldValue) => {
console.log(`Search changed from "${oldValue}" to "${newValue}"`)
// This is where you'd call an API
})
</script>
watch takes the reactive source you want to track, and a callback that runs whenever it changes. The callback receives the new value and the old value.
There’s also watchEffect, which runs immediately and automatically tracks any reactive dependencies used inside it:
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log(`Count is now: ${count.value}`)
// Runs immediately, then again whenever count changes
})
Use watch when you need to compare old and new values, or only run on specific changes. Use watchEffect when you want to react to any reactive state your code touches.
Seeing It All Together
Here’s a small component that uses everything from this article:
<script setup>
import { ref, computed, watch } from 'vue'
const items = ref(['apples', 'bananas', 'carrots'])
const newItem = ref('')
const itemCount = computed(() => items.value.length)
function addItem() {
if (newItem.value.trim()) {
items.value.push(newItem.value.trim())
newItem.value = ''
}
}
watch(itemCount, (count) => {
if (count >= 5) {
console.log('You have a full shopping list!')
}
})
</script>
<template>
<div>
<h2>Shopping List ({{ itemCount }} items)</h2>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<input v-model="newItem" placeholder="Add an item" />
<button @click="addItem">Add</button>
</div>
</template>
A list of items, a computed count, an input that adds to the list, and a watcher that logs when you have five or more. Each piece of the reactivity system playing its role.
The Mental Model
Here’s the simplest way to think about Vue’s reactivity:
ref/reactive→ Declare state that Vue should watchcomputed→ Derive new values from existing statewatch/watchEffect→ Run side effects when state changes- Template bindings → Automatically connect state to the UI
You don’t tell Vue when to update. You describe what depends on what, and Vue figures out the rest.
Your Turn
Open the shopping list component above and try these challenges:
- Add a
refcalledfilterand acomputedproperty that returns only items containing the filter string - Add an input bound to
filterwithv-modelso users can search the list in real time - Add a
watchonfilterthat logs"Filtering for: <term>"each time it changes
If you can do all three — you’ve internalized the entire reactivity system.
What’s Next
In Article 4, we get into components — the reusable building blocks of every Vue application. We’ll cover props, events, and why Single-File Components are the right way to structure your code.
You understand the engine now. Time to build with it.
This is Part 3 of the Vue 3 Fundamentals series. Sources: Vue.js Reactivity Fundamentals, Vue School Articles.