Modern web apps are not only required to be responsive but also proactive in anticipating users’ changing requirements. For that, you need a dynamic user interface that automatically updates according to data changes without interrupting the app’s performance. This may seem like a challenging undertaking; however, hiring a top Vue development company can make the process straightforward and maximize outcomes.
They leverage VueJS 3’s reactivity system to achieve these goals. It is highly efficient as it works on a granular level to manage your app’s state, providing greater flexibility and control than a standard reactivity approach. This article explores the concept of the reactivity system in Vue 3, discussing what it is and how it works.
1. What is Reactivity in VueJS?
In Vue.js, the reactivity system tracks data changes and instantly updates the page. Developers modify state objects, and the framework refreshes only the affected parts. This approach saves time and reduces the need for direct DOM manipulation.
Vue’s reactivity system combines a virtual DOM, dependency tracking, and JavaScript’s getter/setter methodologies. Enabling the reactivity system in Vue is easy. You only need to define the data property within the data option of Vue’s instance or use the reactive function from Vue composition API to make it reactive.
At the foundation of the reactivity system lies a JavaScript proxy object. This system tracks only the properties accessed during the rendering process. So, the reactivity is triggered only by the data that is actually used in the computed properties or templates.
2. Key Benefits of Vue.js 3 Reactivity
Using Vue’s reactivity systems offers benefits such as seamless UI updates, improved performance, declarative rendering, and efficient DOM updates. Let us understand them in detail.
- Seamless UI Updates: Reactivity System from Vue ensures that the user interface stays in sync with the data changes. It allows you to focus only on defining components and the data, while Vue handles everything else. This is helpful in writing clean and concise code.
- Enhanced Performance: Vue’s reactivity system is designed for efficiency. It identifies and updates only the essential UI components whendata occur, avoiding unnecessary renders, improving the overall app performance.
- Simplified State Management: Vue.js enables developers handle application state effortlessly. When they change the reactive state, the interface refreshes at once. Composition API features like ref and reactive keep code organized and manageable in large projects.
- Enhanced Developer Experience: Vue.js offers clear guides that make learning smooth. Developers use Vue Devtools to monitor components and reactive data, helping them identify and resolve issues quickly.
- Deep Reactivity: In Vue.js, all data, including nested objects and arrays, reacts automatically. When values change, the framework updates the view without requiring extra code or manual intervention.
3. Core Reactivity APIs
Core APIs serve as the foundation of Vue’s reactivity system because they help create reactive state, derive reactive values, and run side effects when state changes.
3.1 reactive()
The reactive () function takes an object or an array and returns a proxy that tracks the changes. Every change in the nested property will be reactive. But if you destructure the reactive object without using the torefs(), the reactivity will break.
<script setup> import { reactive } from 'vue'; const user = reactive({ name: 'Jay', age: 25 }); const incrementUserAge = () => { user.age++ } </script> <template> <div>Age: {{user.age}}</div> <button @click="incrementUserAge">Age++</button> </template> |
In the above example, the reactive () function takes a plain JavaScript object and wraps it in a proxy. This proxy is then used to track any changes to or access of the object’s properties. The object taken by the reactive() in the above code is the user, making it a reactive object.
As a result, both user.age and the user.name becomes reactive. So, when Vue renders the template, it reads user.age and tracks its dependency. The code shows that clicking the button will trigger the incrementUserAge function.
Since it’s a reactive property, the user.age++ will change. Proxy from the reactive() detects this change, prompting Vue to check the UI elements depending on the user.age. Vue will re-render only the dependent Ui components while the <div> updates with the new age.
3.2 ref()
The ref() function takes an inner value and returns a reactive reference object with a “.value” property. It can hold any type of value, including primitives, objects, and arrays. When the value is an object, it automatically becomes reactive. Use ref() when you need a single reactive value or plan to replace the entire value later.
<script setup> import { ref } from 'vue'; const count = ref(0); const increment = () => { count.value++; } </script> <template> <div>{{count}}</div> <button @click="increment">Count++</button> </template> |
3.3 computed()
You can create a reactive value derived from other reactive sources using computed (). The results are cached automatically and recalculated only when any changes are identified in dependencies. Depending on whether getters and setters are used, the computed values are either read-only or writable. These values are not to be used for methods or watchers but for the derived state.
<script setup> import { computed, ref } from 'vue'; const count = ref(0); const doubleCount = computed (() => count.value*2); const increment = () => { count.value++; } </script> <template> <div>Count: {{count}}</div> <div>Double: {{doubleCount}}</div> <button @click="increment">Count++</button> </template> |
Writable example:
You can both read and update the drive state using a writable computed property, as it allows you to define the get and set functions.
<script setup> import { computed, onMounted, ref } from 'vue'; const count = ref(0); const doubleCount = computed({ get: () => count.value * 2, set: (value) => { count.value = ( value / 2) } }) onMounted(() => { doubleCount.value = 10; }) const increment = () => { count.value++; } </script> |
3.4 readonly()
readonly() produces a deep, immutable proxy for a reactive object or ref. In production, any mutation attempt fails silently, while in development, it will trigger a warning. It is beneficial for safely exposing the state.
<script setup> import { reactive, readonly } from 'vue'; const state = reactive({ count: 0 }); const publicState = readonly(state); const incrementPublicCount = () => { publicState.count++ } </script> <template> <div>Count: {{state.count}}</div> <div>Public count: {{publicState.count}}</div> <button @click="incrementPublicCount">public count++</button> </template> |

3.5 watchEffect()
watchEffect() automatically tracks every reactive dependency accessed during its execution and runs the function immediately. Whenever the dependency changes, it re-runs automatically. You don’t even need to specify sources explicitly. It is best for simple side effects without old/new values.
<script setup> import { watchEffect } from 'vue'; const count = ref(0); watchEffect(() => { console.log("Count changed", count.value) }) const incrementCount = () => { count.value++ } </script> <template> <div>Count: {{state.count}}</div> <button @click="incrementCount">public count++</button> </template> |
3.6 watch()
watch() observes one or more reactive sources and runs a callback when they change. Along with the support options like immediate, deep, and flush, it also provides access to both old and new values. The watch() function is primarily used for API calls, async tasks, and complex side effects.
<script setup> import { ref, watch } from "vue"; const count = ref(0); watch(count, (newVal, oldVal) => { console.log(newVal, oldVal); }); const incrementCount = () => { count.value++; }; </script> |
3.7 onWatcherCleanup()
onWatcherCleanup() registers a cleanup callback inside watch() or watchEffect(). The cleanup runs before the watcher re-runs or stops, making it ideal for clearing timers, aborting requests, or removing listeners.
watch(count, (newVal, oldVal) => { console.log(newVal, oldVal); const timer = setInterval(() => {}, 1000); onWatcherCleanup(() => clearInterval(timer)); }); |
4. Reactivity Utilities
These utilities from the reactivity system inspect, wrap, or preserve reactivity when working with refs and reactive objects.
4.1 isRef()
isRef() checks if the value is a ref object. Composables and generic utilities have inputs with refs or plain values, making them an ideal use case for isRef().
import { isRef, ref } from 'vue' isRef(ref(1)) // true isRef(1) // false |
4.2 unRef()
UnRef() returns the inner value if the argument is a ref. If not, then it returns the value itself. This function helps create a program that works seamlessly with both refs and non-refs.
import { unref, ref} from 'vue' const count = ref(5); const unrefCount = unref(count); // 5 |
4.3 toRef()
This function is used to build a ref linked with the property on the reactive object. It keeps both the ref and the object in sync, ensuring that the other side is automatically updated when changes are detected in either.
import {reactive, toRef } from 'vue' const state = reactive({ count: 0 }) const count = toRef(state, 'count') |
4.4 toValue()
No matter if it’s a getter, ref, or a value, toValue() normalizes the input and returns its current value. This function proves highly beneficial in composables.
import {toValue } from 'vue' toValue(5) toValue(ref (5)) toValue(() => 5) |
4.5 toRefs()
Destructuring can cause an object to lose its reactivity. The only way to perform destructuring without losing reactivity is to use a toRefs() function to convert the reactivity object into an object of refs.
import {reactive, toRefs } from 'vue' const state = reactive({ a: 1, b: 2 }) const { a, b } = toRefs(state) |
4.6 isProxy()
Checks if the reactive or readOnly functions have created any proxy object.
import {reactive, isProxy } from 'vue' const state = reactive({ a: 1, b: 2 }) const isStateProxy = isProxy (state) // true |
4.7 isReactive()
Checks if an object is a reactive proxy created by reactive().
import {reactive, isReactive } from 'vue' const state = reactive({ a: 1, b: 2 }) const isReactiveProxy = isReactive(state) // true |
4.8 isReadonly()
Checks if an object is a read-only proxy created by readonly() or shallowReadonly().
import { reactive, readonly, isReadonly } from 'vue' const state = readonly (reactive({ a: 1, b: 2 })) const isStateReadonly= isReadonly(state) // true |
5. Advance Reactivity APIs
The advanced reactivity APIs are high-level performance optimization APIs used when dealing with complex data structures or during integration with external state management systems.
5.1 shallowRef()
It is a shallow version of the ref() that doesn’t track nested properties. In shallowRef(), only value access is reactive.
import { shallowRef } from 'vue' const state = shallowRef({ count: 1 }) // does NOT trigger change state.value.count = 2 // does trigger change state.value = { count: 2 } |
5.2 triggerRef()
It manually triggers effects dependent on shallowRef and allows you to mutate nested data.
import { shallowRef, triggerRef } from 'vue' const state = shallowRef({ count: 1 }) state.value.count++ triggerRef(state) |
5.3 customRef()
Mostly used for throttling inputs or debouncing, customRef() gives total control over dependency tracking and triggering.
<script setup> import { customRef } from "vue"; function useCustomCount(initialValue = 0) { return customRef((track, trigger) => ({ get() { track() // track dependency return initialValue }, set(value) { initialValue = value // update value trigger() // notify Vue to update UI } })) } const count = useCustomCount(0); const incrementCount = () => { count.value++ }; </script> <template> <div>Count: {{ count }}</div> <button @click="incrementCount">Count++</button> </template> |
5.4 shallowReactive()
A shallow version of reactive() where only root-level properties are reactive.
import { shallowReactive } from "vue"; const state = shallowReactive({nested: {count: 0}}); // does not trigger change state.nested.count++; // does trigger change state.nested = { count: 5 } |
5.5 shallowReadonly()
A shallow version of readonly() that prevents mutation only at the root level.
import { shallowReadonly } from "vue"; const state = shallowReadonly({ count1: 0, nested: { count2: 0 } }); // this will NOT work (readonly) state.count1++ // this will work state.nested.count2++ |
5.6 toRaw()
Returns the original non-proxy object from a reactive or readonly proxy.
import {reactive, toRaw } from "vue"; const state = reactive({ count: 5 }); toRaw(state) // { count: 5 } |
5.7 markRaw()
Marks an object so Vue will never convert it into a reactive proxy.
import { is Reactive, markRaw } from "vue"; const bar = markRaw({ count: 5 }) isReactive (bar) // false |
5.8 effectScope()
It is a disposable box for reactivity that groups multiple reactive effects to dispose of them all together.
import { computed, effectScope, watch, watchEffect } from "vue"; const scope = effectScope(); scope.run(() => { const doubled = computed(() => counter.value * 2) watch(doubled, () => console.log(doubled.value)) watchEffect(() => console.log('Count: ', doubled.value)) }) // to dispose all effects in the scope scope.stop() |
5.9 getCurrentScope()
Returns the currently active effect scope, if one exists.
const scope = effectScope(); scope.run(() => { console.log(getCurrentScope() === scope); // true }) console.log(getCurrentScope() === scope); // false |
5.10 onScopeDispose()
When the active effect scope is disposed, onScopeDispose registers the cleanup function to run.
<script setup> import { ref, watch, effectScope, onScopeDispose } from 'vue' const count = ref(0) const scope = effectScope() scope.run(() => { // watcher belongs to this scope watch(count, (val) => { console.log('Count changed:', val) }) // cleanup logic for this scope onScopeDispose(() => { console.log('Scope disposed: cleanup executed') }) }) const increment = () => { count.value++ } const stopScope = () => { scope.stop() // triggers onScopeDispose } </script> <template> <div>Count: {{ count }}</div> <button @click="increment">Increment</button> <button @click="stopScope">Stop Scope</button> </template> |
6. Conclusion
The reactivity system is a core concept in Vue that ensures seamless synchronization between UI and data. Understanding this reactivity model, how to work with reactive data, and use computed properties helps developers create highly responsive and dynamic Vue applications. For optimal performance and reactivity, do not forget to follow the best practices. In case you have any queries or are facing any particular challenges in your Vue project, feel free to contact our experts, and we will get back to you as soon as possible.

Comments
Leave a message...