Vue Reactivity In-Depth

How Vue Handles Reactivity

Hossein Mousavi
ITNEXT

--

Welcome to “Vue 3 Reactivity In Depth.” Let’s dive into how Vue 3 makes things happen behind the scenes, making your web development experience smoother and more dynamic.

Table Of Contents

I. An Introduction To Reactivity
II. Core Concepts
III. How does Vue’s Reactivity Work Under The Hood?
IV. Advanced Techniques
V. Best Practices and Pitfalls

I. An Introduction To Reactivity:

Photo by Milad Fakurian on Unsplash

What is Reactivity, and why is it important in Vue 3?

Reactivity, the system’s ability to detect and respond to data changes, is central to Vue 3’s seamless data and UI synchronization.

Leveraging JavaScript’s Proxy API, Vue 3 employs a robust reactivity system, enabling direct observation for efficient updates within components.

Vue 3’s reactivity streamlines development, eliminating the need for manual DOM manipulation. It enhances performance through selective updates, optimizing rendering for a better user experience. Encouraging declarative code, Vue 3 allows developers to focus on data relationships and business logic, contributing to more concise and maintainable code.

Proactive Reactivity in Vue 3: A Shift from Two-Way Binding

Traditional frameworks rely on two-way data binding, potentially causing performance issues and subtle bugs. Vue 3 introduces a proactive reactivity approach, where data within components is observed for changes. This shift brings benefits:

  • Real-time updates for dynamic interfaces.
  • Effortless management of complex state relationships.
  • Optimized updates enhance overall application responsiveness.
  • Simplified development focuses on core logic, improving code maintainability.

II. Core Concepts

Photo by Hedi Alija on Unsplash

ref() and reactive(): Creating reactive data structures

In Vue 3, you can create reactive data structures using ref() for primitive values and reactive() for objects.

  • ref<T>(value: T): Holds a single reactive reference of type T.
  • reactive<T>(obj: T): Creates a reactive version of an object or array of type T, tracking changes within its properties.
// ref
const counter = ref<number>(0);

// reactive
interface User {
name: string;
age: number;
}

const users = reactive<Array<User>>([
{ name: "user1", age: 20 },
{ name: "user2", age: 30 },
]);

While ref() is suitable for primitive values, reactive() is ideal for complex objects, offering a comprehensive reactivity setup.

Computed properties and their role in derived data

You can use computed properties to compute derived data, efficiently enhancing your application’s responsiveness, and it automatically updates its value whenever our reactive has been changed.

const userGroup = computed<string>(() => `${userData.name} groupName`);

Watchers: Tracking changes and side effects

Watchers allow you to observe changes in reactive properties and execute custom logic when those properties change. This mechanism is particularly useful when you need to perform specific actions in response to changes in your data.

watch(() => userData.age, (newAge: number, oldAge: number) => {
console.log(`Age changed from ${oldAge} to ${newAge}`);
});

III. How does Vue’s Reactivity Work Under The Hood?

Photo by Jakob Owens on Unsplash

In JavaScript, property access interception can be accomplished through two main mechanisms.

  • Getter / Setter (Vue 2)
  • Proxy (Vue 3)

Vue 2 predominantly relied on getters/setters, as Proxies had limited support across browsers then. However, with the advancements in browser compatibility, Vue 3 introduces a more versatile approach by utilizing proxies for reactive objects while still employing getters/setters for refs. The following pseudo-code provides a conceptual illustration of their functionality:

const reactiveData = reactive<Record<'message', string>>({
message: 'Hello, Vue!',
});

For more info about this you can checkout the Vue’s official doc here.

Vue boasts two approaches to reactivity:

  • Runtime reactivity: The most common is that it dynamically tracks changes during component execution. Every data access involves proxy interaction, making it flexible but potentially slower.
  • Compile-time reactivity: Used with certain compiler options, it analyzes templates and statically determines dependencies. This reduces runtime overhead but may limit flexibility.

IV. Advanced Techniques

Photo by Michael Dziedzic on Unsplash

Deep Reactivity And Nested Objects

Consider a deeply nested object in your component’s data like the one below:

const deepData = reactive({
level1: {
level2: {
level3: 'Nested Value',
},
},
});

Now, let’s modify a deeply nested property:

deepData.level1.level2.level3 = 'Modified Value';

Vue automatically tracks changes within nested objects and arrays, ensuring dependent elements update seamlessly.

Be mindful of potential performance implications with deeply nested structures. Using shallowRef for selective reactivity or alternative approaches like Immer can help optimize performance.

Reactivity In Conditional Rendering And Loops

Reactivity in dynamic scenarios, such as conditional rendering and loops, is crucial for maintaining an up-to-date user interface.

<template>
<div>
<p v-if="showText">{{ text }}</p>
<button @click="toggleContent">Toggle Text</button>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const showText = ref<boolean>(true);
const text = 'dastan';

function toggleText(): void {
showText.value = !showText.value;
};

</script>

The same applies when using reactive variables in a loop using v-for with just one consideration:

Strategically using reactive() and ref() ensures optimal reactivity performance within loops.

Form Handling And Input Reactivity

Check out this code:

<template>
<div>
<input
type="text"
v-model="name"
@input="handleNameInput"
placeholder="Enter Name"
/>

<p v-if="isNameEmpty">Username cannot be empty</p>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const name = ref<string>('');
const isNameEmpty = computed<boolean>(() => name.value.trim() === '');

function handleNameInput(event): void {
name.value = event.target.value;
};

</script>

We have a two-way data binding using the v-model We have also implemented a simple built-in validation to avoid an empty name in our application.

Customizing Reactivity Behavior With Composables

A composable is a function that leverages the Composition API to encapsulate and reuse stateful logic.

Composables empower you to write modular, reusable, and maintainable reactive code.

We can write down our code in the composables and import them into our component.

// our composable: useMessage.ts
import { ref, onMounted } from 'vue';

export default function useMessage(initialMessage: string) {
const message = ref<string>(initialMessage);

onMounted(() => {
setTimeout(() => {
message.value = 'Delayed Message';
}, 2000);
});

return { message };
}
// Component using the composable
<script>
import useMessage from './useMessage';

export default {
setup() {
const { message } = useMessage('Hello, Vue!');

return { message };
},
};
</script>

Feel free to use the message variable in your component’s template or script.

Best Practices and Pitfalls

Photo by Rostyslav Savchyn on Unsplash

Vue’s reactivity system thrives on proper care and attention like any powerful tool. Let’s waltz through some best practices and pitfalls to ensure your reactive applications shine without performance hiccups or memory leaks.

Optimizing Performance with Memoization and Caching

Optimizing performance in Vue 3 involves strategically using memoization and caching techniques. This ensures that expensive computations are only recalculated when necessary, enhancing overall application efficiency.

Memoization

Memoization involves storing the results of expensive function calls and returning the cached result when the same inputs occur again. In the context of Vue 3, this can be particularly useful for computed properties and functions within composables.

setup() {
const expensiveCalculation = computed(() => {
// Perform complex calculation
return memoizedResult;
});

return { expensiveCalculation };
},

Caching

Caching involves storing frequently used data preventing unnecessary recalculations or fetches. This is beneficial for scenarios like data fetching, where the same data might be requested multiple times.

const dataCache = ref<ResponseData>({});

async function fetchData(key: string): Promise<ResponseData> {
if (key in dataCache.value) {
return dataCache.value[key];
}

const result = await fetchDataFromServer(key);
dataCache.value[key] = result;
return result;
}

Avoiding common reactivity mistakes and memory leaks

While Vue 3’s reactivity system is powerful, certain practices can lead to common mistakes and potential memory leaks.

  • Over-reactivity: Avoid wrapping every data piece in reactivity. Only use it where updates directly impact the UI. Unnecessary reactivity bogs down performance. Think of it as adding spotlights to every prop in the room, blinding you with unnecessary light.
  • Deep Reactivity Dives: Remember nested reactivity, especially with large objects or arrays. Consider shallow reactivity or alternative approaches like Immer for deeply nested structures. Think of it as focusing on specific dance steps within a routine instead of trying to illuminate every muscle twitch.
  • Memory Leaks: Improper cleanup of subscriptions and watchers can lead to memory leaks. Always unmount components and remove watchers when no longer needed. Think of it as bowing gracefully at the end of the dance, ensuring the stage is cleared for the next act.

--

--