NJ
Reactivity in Vue 3 — How Vue Makes Your UI Feel Alive | Nerando Johnson
Vue.js logo on a light blue background with geometric shapes Vue, JavaScript, Tutorials

Reactivity in Vue 3 — How Vue Makes Your UI Feel Alive

Nerando Johnson
#vue3 #javascript #frontend #tutorial #reactivity #composition-api

6 min read

3 views

Part 3 of 3

Vue 3 Fundamentals

This article is part of the Vue 3 Fundamentals series.

Progress 100% Complete

Reactivity 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?

The magic of things updating automatically

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

ref — your go-to reactive 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

reactive — for when your state is a team sport

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

computed — let Vue do the math for you

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

watch — keeping an eye on your state

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

All the pieces clicking into place

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

Breaking it all down into a simple mental model

Here’s the simplest way to think about Vue’s reactivity:

  • ref / reactive → Declare state that Vue should watch
  • computed → Derive new values from existing state
  • watch / 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

Your turn to put it into practice

Open the shopping list component above and try these challenges:

  1. Add a ref called filter and a computed property that returns only items containing the filter string
  2. Add an input bound to filter with v-model so users can search the list in real time
  3. Add a watch on filter that logs "Filtering for: <term>" each time it changes

If you can do all three — you’ve internalized the entire reactivity system.

What’s Next

What's coming up next in the series

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.

Part 3 of 3

Vue 3 Fundamentals

This article is part of the Vue 3 Fundamentals series.

Progress 100% Complete