Suggest an editImprove this articleRefine the answer for “How does reactivity work in Vue.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Vue.js reactivity** tracks which data a component reads during render via ES6 `Proxy` and re-runs only the components that depend on what changed. ```js const count = ref(0) // ref for primitives - access via .value const state = reactive({ n: 0 }) // reactive for objects - direct access count.value++ // Proxy setter fires, dependent templates re-render state.n++ // same Proxy mechanism ``` **Key point:** `ref()` for primitives, `reactive()` for objects. Vue 3 `Proxy` catches all property changes including dynamically added ones; Vue 2 `Object.defineProperty` did not.Shown above the full answer for quick recall.Answer (EN)Image**Vue.js reactivity** automatically tracks which data your component reads during render and re-runs only the parts that depend on data that changed. ## Theory ### TL;DR - Analogy: a spreadsheet formula. Change one cell, every formula that references it recalculates on its own. - Core mechanism: `Proxy` intercepts property reads and writes, builds a dependency graph per render, triggers only affected components. - Vue 3 uses ES6 `Proxy`; Vue 2 used `Object.defineProperty` and missed dynamically added properties. - Decision rule: `ref()` for primitives and simple values; `reactive()` for complex nested objects. - Templates auto-unwrap `ref`, so no `.value` needed in HTML. ### Quick example ```vue <script setup> import { ref } from 'vue' const count = ref(0) // Reactive primitive - wrapped in .value const increment = () => count.value++ // .value triggers the Proxy setter </script> <template> <!-- Template auto-unwraps ref - no .value here --> <button @click="increment">Count: {{ count }}</button> </template> ``` When you click the button, `count.value++` hits the Proxy setter. Vue finds every template and computed that read `count` during the last render and re-runs them. Nothing else updates. ### How the Proxy tracks dependencies During render, Vue wraps your data in an ES6 `Proxy`. Every time a template or computed reads a property, the Proxy fires a `track()` call that registers: "this effect depends on this property." Every write fires `trigger()` that schedules re-renders for all registered effects. Internally, dependencies live in a `WeakMap<object, Map<key, Set<effect>>>`. That structure lets Vue look up exactly which effects to re-run for a specific property change, without touching unrelated state. The whole system runs in plain browser JS, no runtime compiler hooks needed. Vue 2 did the same with `Object.defineProperty` getters and setters. That worked for pre-defined properties but missed new properties added after initialization and direct array index assignments like `arr[0] = x`. The `Proxy` approach in Vue 3 covers all of that transparently. ### Key difference between ref() and reactive() `ref()` wraps any value (primitive or object) in a container object with a `.value` property. The Proxy sits on that container. `reactive()` converts the entire object into a Proxy directly, so you access properties without `.value`. Both give deep reactivity for nested objects. The practical split: use `ref` for things you might replace entirely (`count.value = 0`), use `reactive` for objects where you mutate properties in place. ### When to use - Primitive (number, string, boolean): `ref()` - the only option that works. - Single value you might replace: `ref()` - `count.value = newCount` stays clean. - Complex nested object with many properties: `reactive()` - no `.value` overhead. - Derived data that should cache: `computed()` - recalculates only when dependencies change. - Async or external data: `ref()` plus `await` handles uninitialized state naturally. - Objects you never want tracked: `markRaw()` - opt out of reactivity entirely. ### Common mistakes **1. Pushing a plain object into a reactive array** ```js // Wrong - the new object is not reactive state.items.push({ count: 0 }) state.items[0].count++ // UI does not update // Fix - wrap in reactive() state.items.push(reactive({ count: 0 })) state.items[0].count++ // Updates correctly ``` The outer array is proxied, but the object you pushed is a plain JS object. Mutations to its properties have no Proxy to intercept them. **2. Forgetting `.value` in script** ```js const count = ref(0) count++ // Wrong - reassigns the Ref object reference count.value++ // Correct ``` Templates auto-unwrap, scripts do not. That asymmetry catches almost everyone at least once. **3. Replacing an entire reactive array** ```js // Wrong - breaks the Proxy chain state.arr = [] // Fix - mutate in place state.arr.length = 0 // or state.arr.splice(0) ``` Reassignment creates a new plain array and drops the Proxy. Anything that was watching the old reference stops updating. **4. Destructuring a reactive object** ```js const state = reactive({ count: 0 }) const { count } = state // count is now a plain number - not reactive // Fix - use toRefs() import { toRefs } from 'vue' const { count } = toRefs(state) // count is now a ref - reactive ``` This is the most common footgun with `reactive()`. It looks a lot like React's destructured `useState`, but the behavior is different. The moment you pull a property out, you lose the Proxy connection. **5. Index keys in v-for** ```html <!-- Wrong - DOM reorder breaks dependency tracking --> <li v-for="(item, i) in list" :key="i"> <!-- Fix - stable identity key --> <li v-for="item in list" :key="item.id"> ``` ### Real-world usage - Nuxt.js: `useState()` wraps `reactive` for SSR-safe shared state across components. - Pinia: `defineStore` uses `ref` and `reactive` as the basis for all store definitions. - VueUse: `useStorage` keeps a `ref` in sync with `localStorage` reactively. - Quasar: form validation uses `reactive` error objects that update inline. - VitePress: reactive search filters on static markdown content via `computed`. ### Follow-up questions **Q:** What is the difference between `ref()` and `reactive()`? **A:** `ref()` wraps any value in a `.value` container and works with primitives. `reactive()` proxies an object directly with no `.value`. Both are deeply reactive. The main split: `ref` when you might replace the value entirely, `reactive` when you mutate properties in place. **Q:** How does Vue detect changes in arrays? **A:** The Proxy traps `length` sets and index assignments directly. Methods like `push`, `pop`, and `splice` mutate the proxied array, so the Proxy intercepts each operation and triggers dependencies. **Q:** What happens when you assign a non-reactive object to a reactive property? **A:** The property value becomes a plain object. Mutations to its nested properties have no Proxy to intercept them, so the UI will not update. Fix: wrap the assigned value with `reactive()` before assigning. **Q:** Why did Vue 3 move from `Object.defineProperty` to `Proxy`? **A:** `Object.defineProperty` only wraps properties that existed at definition time. Adding a new property later, or setting an array by index (`arr[0] = x`), bypassed the setter entirely. `Proxy` wraps the object itself, so any operation including new properties, deletions, and index sets gets intercepted automatically. **Q:** How would you debug lost reactivity in a large production app? **A:** Start with Vue DevTools and inspect component state. If a value shows there but the UI does not update, the template reference is stale. Check for destructured reactive objects missing `toRefs`. Look for `markRaw()` calls that might have opted out a shared object. The DevTools timeline shows which effects fired, which narrows down where the dependency chain broke. ## Examples ### Basic: counter with ref ```vue <script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <button @click="count.value++">Clicked {{ count }} times</button> </template> ``` `count` starts at `0`. Each click increments `.value`, the Proxy setter fires, Vue re-renders the button text. The template uses `{{ count }}` without `.value` because templates auto-unwrap refs. ### Intermediate: todo list with nested reactive state ```vue <script setup> import { reactive, ref } from 'vue' const todos = reactive({ list: [], filter: 'all' }) const newTodo = ref('') const addTodo = () => { if (!newTodo.value.trim()) return todos.list.push({ id: Date.now(), text: newTodo.value, done: false }) newTodo.value = '' } const toggle = (id) => { const todo = todos.list.find(t => t.id === id) if (todo) todo.done = !todo.done } </script> <template> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="New task" /> <ul> <li v-for="todo in todos.list" :key="todo.id"> <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" /> <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }"> {{ todo.text }} </span> </li> </ul> </template> ``` `todos` is a `reactive` object, so `todos.list`, `todos.filter`, and every property of items inside the list are all proxied. Adding an item updates the list. Toggling `done` crosses out the text. Notice `todo.id` as the key instead of the index - this prevents stale DOM references when items reorder. ### Advanced: computed with dependency caching ```vue <script setup> import { reactive, computed } from 'vue' const todos = reactive({ list: [ { id: 1, text: 'Buy milk', done: false }, { id: 2, text: 'Write tests', done: true } ] }) // Recomputes only when todos.list changes const remaining = computed(() => todos.list.filter(t => !t.done).length ) const completeAll = () => { todos.list.forEach(t => { t.done = true }) } </script> <template> <p>{{ remaining }} tasks left</p> <button @click="completeAll">Complete all</button> </template> ``` `computed()` registers its own effect during evaluation. The first time `remaining` is read, it tracks every property access inside the getter. After `completeAll` sets all `done` to `true`, Vue triggers the `remaining` effect, recomputes the value, and updates the paragraph. If nothing in `todos.list` changed between renders, `computed` returns the cached result without re-running the filter.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.