This episode dives deep into reactive state management in Vue 3: ref and reactive, computed properties, watchers, lifecycle hooks, and a comparison of the Composition API and Options API, ending with building custom composables for reusable logic.

In episode 4 you rendered data and responded to events. Now it's time to understand the brain of an application: reactive state. How well you manage state determines how maintainable the application is — and the Composition API in Vue 3 is designed so state and logic can be grouped together neatly.
Episode 5 dissects ref and reactive, computed for derived values, watchers for reacting to changes, and lifecycle hooks. Then we'll compare the Composition API and the Options API, and close with custom composables — a technique you'll use in almost every coding episode that follows.
ref wraps any value into a reactive one. Access the value through the .value property:
import { ref } from "vue";
const jumlah = ref(0);
jumlah.value = 5;When jumlah.value changes, every part of the template that depends on it updates too. A ref works for primitives as well as objects, and is always safe to destructure because its value is accessed through .value.
reactive wraps an object so its properties become reactive directly:
import { reactive } from "vue";
const user = reactive({
nama: "Arman",
skill: ["Vue", "Node.js"],
});
user.nama = "Budi";
user.skill.push("TypeScript");The modifications user.nama = "Budi" and user.skill.push("TypeScript") are registered by the reactivity system right away, including changes inside arrays. Use reactive for objects with many fields, and ref for single values or when you need the automatic unwrapping in templates.
computed creates a cached derived value that is only recalculated when its dependencies change:
import { ref, computed } from "vue";
const harga = ref(200000);
const diskon = ref(0.1);
const total = computed(() => harga.value * (1 - diskon.value));total updates automatically when harga or diskon changes, without wasteful recalculations. Because it's cached, computed is more efficient than calling a plain function in a template.
watch reacts to one specific change; watchEffect tracks every dependency it reads and runs immediately:
import { ref, watch, watchEffect } from "vue";
const query = ref("");
watch(query, (nilaiBaru) => {
console.log("query berubah:", nilaiBaru);
});
watchEffect(() => {
console.log("query saat ini:", query.value);
});The key difference: watch(query, ...) only fires when query changes, while watchEffect(...) runs once at the start and then every time any of its dependencies change.
Lifecycle hooks execute at specific moments in a component's lifecycle:
import { onMounted, onUnmounted, watchEffect } from "vue";
onMounted(() => {
console.log("komponen siap");
});
onUnmounted(() => {
console.log("komponen dilepas");
});onMounted is used for initialization such as API calls, and onUnmounted for cleanup such as removing intervals or listeners.
The Options API organizes logic into the data, computed, and methods options:
<script>
export default {
data() {
return { jumlah: 0 };
},
methods: {
tambah() {
this.jumlah = this.jumlah + 1;
},
},
};
</script>The Composition API organizes everything within the setup function or <script setup>:
<script setup>
import { ref } from "vue";
const jumlah = ref(0);
function tambah() {
jumlah.value = jumlah.value + 1;
}
</script>The Composition API is more flexible for related logic because you can group code by function rather than by type. The Options API is easier to read for small components and is still fully supported. Both are valid; this series uses the Composition API because it fits large applications and composables better.
A composable is a function that uses the Composition API and returns reusable state. The most classic example: a counter and timer:
import { ref, computed } from "vue";
export function useCounter(awal = 0) {
const jumlah = ref(awal);
const ganda = computed(() => jumlah.value * 2);
function tambah() {
jumlah.value = jumlah.value + 1;
}
return { jumlah, ganda, tambah };
}Save it as src/composables/useCounter.js. The naming convention for composables always starts with use, so you can recognize them at a glance.
useCounter(10) creates a new counter instance with an initial value of 10, and every consuming component gets its own separate state.
Episode 5 taught you to manage state properly: ref and reactive for basic state, computed for efficient derived values, watchers for reacting to changes, lifecycle hooks for initialization and cleanup, and composables for reusable logic.
Key takeaways:
ref for single values, reactive for objects.computed is cached and only recalculated when dependencies change.watch monitors specific changes; watchEffect tracks everything.onMounted for initialization, onUnmounted for cleanup.use wrap reusable logic.In the next episode 6, we'll cover component communication — props and custom events with defineEmits, provide/inject for dependency injection, slots and scoped slots, plus reusable component patterns for consistent UIs.