Vue.js 3: Building Reactive UIs


I reach for Vue when I want a productive SPA without ceremony. Vue 3's Composition API gave me the grouping-by-feature style I liked from React hooks, with reactivity that often feels lighter for form-heavy UIs.
This post is not "Vue vs React winner takes all." It is how I build a small reactive UI with ref, computed, and watch — and when that choice still makes sense for me.
Options API: data, methods, computed in one object. Familiar if you learned Vue 2.
Composition API: setup() or <script setup> — logic grouped by feature, reusable composables.
When I use Composition API:
useAuth, useCart)When Options API is fine:
Red flag: Rewriting a stable Options API app to Composition API for fashion. Migrate when you touch the file for a real reason.
ref — reactive primitive values and object references.
computed — derived state; caches until dependencies change.
watch — side effects when something changes (API calls, localStorage).
<script setup>
import { ref, computed, watch } from "vue";
const todos = ref([]);
const newTodo = ref("");
const filter = ref("all");
const visibleTodos = computed(() => {
if (filter.value === "active") return todos.value.filter((t) => !t.done);
if (filter.value === "done") return todos.value.filter((t) => t.done);
return todos.value;
});
function addTodo() {
const text = newTodo.value.trim();
if (!text) return;
todos.value.push({ id: Date.now(), text, done: false });
newTodo.value = "";
}
watch(
todos,
(val) => localStorage.setItem("todos", JSON.stringify(val)),
{ deep: true }
);
</script>
<template>
<div>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a todo" />
<button @click="addTodo">Add</button>
<ul>
<li v-for="todo in visibleTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done" />
<span :class="{ 'line-through': todo.done }">{{ todo.text }}</span>
</li>
</ul>
</div>
</template>
What: visibleTodos recalculates when todos or filter changes — no manual sync.
Why: Less boilerplate than syncing derived lists in event handlers.
Red flag: Deep watch on huge objects — performance and debugging pain. Watch specific fields when you can.
| I pick Vue when… | I pick React when… |
|---|---|
| Greenfield SPA, team knows Vue | Next.js App Router, RSC, Vercel deploy |
| Heavy forms, v-model ergonomics | Huge ecosystem package I need |
| Nuxt for SSR with Vue DX | Company standard is React |
No tribal war. Both are tools. I optimize for team velocity and hiring pool on client work.
When two components share fetch + filter logic, I extract a composable:
// composables/useTodos.js
import { ref, computed } from "vue";
export function useTodos() {
const todos = ref([]);
const remaining = computed(() => todos.value.filter((t) => !t.done).length);
function add(text) {
todos.value.push({ id: Date.now(), text, done: false });
}
return { todos, remaining, add };
}
Same idea as React custom hooks — keeps components thin.
todos.value = [...] clarity helped.computed once I thought in derived state.Build one small feature with <script setup>, one computed, one watch. Feel where reactivity saves clicks and where it hides bugs.
Vue 3 is not niche — it is a deliberate choice for approachable reactivity. If your stack is React + Next.js, that is fine too. Know why you picked the tool, not just the logo.