Reactivity Fundamentals | Vue.js (2023)

API Preference

This page and many other chapters later in the guide contain different content for Options API and Composition API. Your current preference is Options APIComposition API. You can toggle between the API styles using the "API Preference" switches at the top of the left sidebar.

Declaring Reactive State

With Options API, we use the data option to declare reactive state of a component. The option value should be a function that returns an object. Vue will call the function when creating a new component instance, and wrap the returned object in its reactivity system. Any top-level properties of this object are proxied on the component instance (this in methods and lifecycle hooks):


export default { data() { return { count: 1 } }, // `mounted` is a lifecycle hook which we will explain later mounted() { // `this` refers to the component instance. console.log(this.count) // => 1 // data can be mutated as well this.count = 2 }}

Try it in the Playground

These instance properties are only added when the instance is first created, so you need to ensure they are all present in the object returned by the data function. Where necessary, use null, undefined or some other placeholder value for properties where the desired value isn't yet available.

It is possible to add a new property directly to this without including it in data. However, properties added this way will not be able to trigger reactive updates.

Vue uses a $ prefix when exposing its own built-in APIs via the component instance. It also reserves the prefix _ for internal properties. You should avoid using names for top-level data properties that start with either of these characters.

Reactive Proxy vs. Original

In Vue 3, data is made reactive by leveraging JavaScript Proxies. Users coming from Vue 2 should be aware of the following edge case:


export default { data() { return { someObject: {} } }, mounted() { const newObject = {} this.someObject = newObject console.log(newObject === this.someObject) // false }}

When you access this.someObject after assigning it, the value is a reactive proxy of the original newObject. Unlike in Vue 2, the original newObject is left intact and will not be made reactive: make sure to always access reactive state as a property of this.

We can create a reactive object or array with the reactive() function:


import { reactive } from 'vue'const state = reactive({ count: 0 })

Reactive objects are JavaScript Proxies and behave just like normal objects. The difference is that Vue is able to track the property access and mutations of a reactive object. If you are curious about the details, we explain how Vue's reactivity system works in Reactivity in Depth - but we recommend reading it after you have finished the main guide.

See also: Typing Reactive

To use reactive state in a component's template, declare and return them from a component's setup() function:


import { reactive } from 'vue'export default { // `setup` is a special hook dedicated for composition API. setup() { const state = reactive({ count: 0 }) // expose the state to the template return { state } }}


<div>{{ state.count }}</div>

Similarly, we can declare functions that mutate reactive state in the same scope and expose them as methods alongside the state:

(Video) Vue 3: Reactivity Made Easy (ref, reactive, toRefs... oh my!)


import { reactive } from 'vue'export default { setup() { const state = reactive({ count: 0 }) function increment() { state.count++ } // don't forget to expose the function as well. return { state, increment } }}

Exposed methods are typically used as event listeners:


<button @click="increment"> {{ state.count }}</button>

<script setup>

Manually exposing state and methods via setup() can be verbose. Luckily, it is only necessary when not using a build step. When using Single-File Components (SFCs), we can greatly simplify the usage with <script setup>:

Try it in the Playground

Top-level imports and variables declared in <script setup> are automatically usable in the template of the same component.

For the rest of the guide, we will be primarily using SFC + <script setup> syntax for Composition API code examples, as that is the most common usage for Vue developers.

Declaring Methods

To add methods to a component instance we use the methods option. This should be an object containing the desired methods:


export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } }, mounted() { // methods can be called in lifecycle hooks, or other methods! this.increment() }}

Vue automatically binds the this value for methods so that it always refers to the component instance. This ensures that a method retains the correct this value if it's used as an event listener or callback. You should avoid using arrow functions when defining methods, as that prevents Vue from binding the appropriate this value:


export default { methods: { increment: () => { // BAD: no `this` access here! } }}

Just like all other properties of the component instance, the methods are accessible from within the component's template. Inside a template they are most commonly used as event listeners:


<button @click="increment">{{ count }}</button>

Try it in the Playground

In the example above, the method increment will be called when the <button> is clicked.

DOM Update Timing

When you mutate reactive state, the DOM is updated automatically. However, it should be noted that the DOM updates are not applied synchronously. Instead, Vue buffers them until the "next tick" in the update cycle to ensure that each component updates only once no matter how many state changes you have made.

To wait for the DOM update to complete after a state change, you can use the nextTick() global API:


import { nextTick } from 'vue'function increment() { state.count++ nextTick(() => { // access updated DOM })}
(Video) Reactivity in Vue 3 - How does it work?


import { nextTick } from 'vue'export default { methods: { increment() { this.count++ nextTick(() => { // access updated DOM }) } }}

Deep Reactivity

In Vue, state is deeply reactive by default. This means you can expect changes to be detected even when you mutate nested objects or arrays:


export default { data() { return { obj: { nested: { count: 0 }, arr: ['foo', 'bar'] } } }, methods: { mutateDeeply() { // these will work as expected. this.obj.nested.count++ this.obj.arr.push('baz') } }}


import { reactive } from 'vue'const obj = reactive({ nested: { count: 0 }, arr: ['foo', 'bar']})function mutateDeeply() { // these will work as expected. obj.nested.count++ obj.arr.push('baz')}

It is also possible to explicitly create shallow reactive objects where the reactivity is only tracked at the root-level, but these are typically only needed in advanced use cases.

Reactive Proxy vs. Original

It is important to note that the returned value from reactive() is a Proxy of the original object, which is not equal to the original object:


const raw = {}const proxy = reactive(raw)// proxy is NOT equal to the original.console.log(proxy === raw) // false

Only the proxy is reactive - mutating the original object will not trigger updates. Therefore, the best practice when working with Vue's reactivity system is to exclusively use the proxied versions of your state.

To ensure consistent access to the proxy, calling reactive() on the same object always returns the same proxy, and calling reactive() on an existing proxy also returns that same proxy:


// calling reactive() on the same object returns the same proxyconsole.log(reactive(raw) === proxy) // true// calling reactive() on a proxy returns itselfconsole.log(reactive(proxy) === proxy) // true

This rule applies to nested objects as well. Due to deep reactivity, nested objects inside a reactive object are also proxies:


const proxy = reactive({})const raw = {}proxy.nested = rawconsole.log(proxy.nested === raw) // false

Limitations of reactive()

The reactive() API has two limitations:

  1. It only works for object types (objects, arrays, and collection types such as Map and Set). It cannot hold primitive types such as string, number or boolean.

  2. Since Vue's reactivity tracking works over property access, we must always keep the same reference to the reactive object. This means we can't easily "replace" a reactive object because the reactivity connection to the first reference is lost:


    (Video) Vue JS 3 Tutorial - 54 - Reactivity and toRefs

    let state = reactive({ count: 0 })// the above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!)state = reactive({ count: 1 })

    It also means that when we assign or destructure a reactive object's property into local variables, or when we pass that property into a function, we will lose the reactivity connection:


    const state = reactive({ count: 0 })// n is a local variable that is disconnected// from state.count.let n = state.count// does not affect original staten++// count is also disconnected from state.count.let { count } = state// does not affect original statecount++// the function receives a plain number and// won't be able to track changes to state.countcallSomeFunction(state.count)

Reactive Variables with ref()

To address the limitations of reactive(), Vue also provides a ref() function which allows us to create reactive "refs" that can hold any value type:


import { ref } from 'vue'const count = ref(0)

ref() takes the argument and returns it wrapped within a ref object with a .value property:


const count = ref(0)console.log(count) // { value: 0 }console.log(count.value) // 0count.value++console.log(count.value) // 1

See also: Typing Refs

Similar to properties on a reactive object, the .value property of a ref is reactive. In addition, when holding object types, ref automatically converts its .value with reactive().

A ref containing an object value can reactively replace the entire object:


const objectRef = ref({ count: 0 })// this works reactivelyobjectRef.value = { count: 1 }

Refs can also be passed into functions or destructured from plain objects without losing reactivity:


const obj = { foo: ref(1), bar: ref(2)}// the function receives a ref// it needs to access the value via .value but it// will retain the reactivity connectioncallSomeFunction( still reactiveconst { foo, bar } = obj

In other words, ref() allows us to create a "reference" to any value and pass it around without losing reactivity. This capability is quite important as it is frequently used when extracting logic into Composable Functions.

Ref Unwrapping in Templates

When refs are accessed as top-level properties in the template, they are automatically "unwrapped" so there is no need to use .value. Here's the previous counter example, using ref() instead:


<script setup>import { ref } from 'vue'const count = ref(0)function increment() { count.value++}</script><template> <button @click="increment"> {{ count }} <!-- no .value needed --> </button></template>

Try it in the Playground

Note that the unwrapping only applies if the ref is a top-level property on the template render context. As an example, object is a top-level property, but is not.

So, given the following object:


const object = { foo: ref(1) }

The following expression will NOT work as expected:


{{ + 1 }}
(Video) Fundamentals of Reactivity in Vue 3

The rendered result will be [object Object]1 because is a ref object. We can fix that by making foo a top-level property:


const { foo } = object


{{ foo + 1 }}

Now the render result will be 2.

One thing to note is that a ref will also be unwrapped if it is the final evaluated value of a text interpolation (i.e. a {{ }} tag), so the following will render 1:


{{ }}

This is just a convenience feature of text interpolation and is equivalent to {{ }}.

Ref Unwrapping in Reactive Objects

When a ref is accessed or mutated as a property of a reactive object, it is also automatically unwrapped so it behaves like a normal property:


const count = ref(0)const state = reactive({ count})console.log(state.count) // 0state.count = 1console.log(count.value) // 1

If a new ref is assigned to a property linked to an existing ref, it will replace the old ref:


const otherCount = ref(2)state.count = otherCountconsole.log(state.count) // 2// original ref is now disconnected from state.countconsole.log(count.value) // 1

Ref unwrapping only happens when nested inside a deep reactive object. It does not apply when it is accessed as a property of a shallow reactive object.

Ref Unwrapping in Arrays and Collections

Unlike reactive objects, there is no unwrapping performed when the ref is accessed as an element of a reactive array or a native collection type like Map:


const books = reactive([ref('Vue 3 Guide')])// need .value hereconsole.log(books[0].value)const map = reactive(new Map([['count', ref(0)]]))// need .value hereconsole.log(map.get('count').value)

Stateful Methods

In some cases, we may need to dynamically create a method function, for example creating a debounced event handler:


import { debounce } from 'lodash-es'export default { methods: { // Debouncing with Lodash click: debounce(function () { // ... respond to click ... }, 500) }}

However, this approach is problematic for components that are reused because a debounced function is stateful: it maintains some internal state on the elapsed time. If multiple component instances share the same debounced function, they will interfere with one another.

To keep each component instance's debounced function independent of the others, we can create the debounced version in the created lifecycle hook:


export default { created() { // each instance now has its own copy of debounced handler this.debouncedClick = _.debounce(, 500) }, unmounted() { // also a good idea to cancel the timer // when the component is removed this.debouncedClick.cancel() }, methods: { click() { // ... respond to click ... } }}
(Video) How does Vue.js reactivity work? Implementing a reactive system - Rubén Valseca - Vue Day 2019


1. When to Use Ref vs. Reactive // Vue Tips
2. Vue JS 3 Reactivity Fundamentals - Composition API
(Marius Espejo)
3. Why you SHOULDN'T use Vue 3 Reactivity Transform!
(Program With Erik)
4. Stability and Reactivity | Chemistry Fundamentals
(Da Chem OG)
5. 11 reactivity fundamentals
(Daily New Learn)
6. JavaScript Reactivity Explained Visually
(Vue Mastery)
Top Articles
Latest Posts
Article information

Author: Patricia Veum II

Last Updated: 18/07/2023

Views: 5552

Rating: 4.3 / 5 (44 voted)

Reviews: 91% of readers found this page helpful

Author information

Name: Patricia Veum II

Birthday: 1994-12-16

Address: 2064 Little Summit, Goldieton, MS 97651-0862

Phone: +6873952696715

Job: Principal Officer

Hobby: Rafting, Cabaret, Candle making, Jigsaw puzzles, Inline skating, Magic, Graffiti

Introduction: My name is Patricia Veum II, I am a vast, combative, smiling, famous, inexpensive, zealous, sparkling person who loves writing and wants to share my knowledge and understanding with you.