watch in vue3 may be a better choice than watchEffect

watch in vue3 may be a better choice than watchEffect

One of the most powerful features of vue is the ability to perform reactive side effects based on changes to underlying data. For this purpose, vue3 provides two methods: watch and watchEffect. Although both methods can monitor changes in reactive data, they have different usage and behaviors. This article will explore the differences between them and when to use them.

watch

The watch method allows us to watch one or more reactive data sources and call a callback function when there is an update.

Although the syntax is different from vue2, its working principle is the same as vue2:

import {<!-- --> watch } from 'vue'
watch(source, callback, options?)

The watch() method accepts 3 parameters:

  • source: Responsive data. Responsive data may be obtained from the following types of data:

    1. The value returned by the getter method or the computed method
    2. a refdata
    3. a reactivedata
    4. An array containing any of the data above
  • callback: Callback function, a function that will be called when the source data changes. The callback function accepts the following parameters:

    • value: observe the new state of the data
    • oldValue: Observe the old state of the data
    • onCleanup: A function that can be used to register a cleanup callback. The cleanup callback will be called before the next re-run of the effect, and can be used to clean up invalid side effects, such as an unresolved asynchronous request.
  • options: Optional configuration, including the following fields:

    • immediate: A Boolean value indicating whether the callback should be triggered immediately when the observer is created, in which case oldValue is undefined
    • deep: A Boolean value indicating whether to perform a deep traversal of the source data. If it is an object, then the callback will start deep listening. View deep monitoring
    • flush: A string indicating how to adjust the callback time. View callback refresh time
    • onTrack / onTrigger: Functions for debugging observers when their dependencies are triggered. View observer debugging

The watch() function is lazy-triggered by default, which means that the callback is only called when the source data being monitored changes.

When listening to multiple sources of data, the callback receives two arrays containing the old and new values corresponding to the source arrays.

Compared to watchEffect, watch allows us to:

  • Lazy triggers side effects
  • More specifically, what state should be used to define and trigger the observer to re-run?
  • Access the previous and current values of the monitored state.

Example

Listen for a getter:

const state = reactive({<!-- --> count: 0 })
watch(
 () => state.count,
 (count, prevCount) => {<!-- -->
 console.log(count, prevCount)
 }
)

Listen for a ref:

const count = ref(0)
watch(count, (count, prevCount) => {<!-- -->
 console.log(count, prevCount)
})

When viewing multiple sources, the callback receives an array containing the old and new values corresponding to the source arrays:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {<!-- -->
 console.log("New values:" , foo, bar)
 console.log("Old values:" , prevFoo, prevBar)
})

WatchEffect

watchEffect will immediately run a callback function and automatically track its passive dependencies. The callback function is executed when any react dependency changes.

import {<!-- --> reactive, watchEffect } from "vue"

const state = reactive({<!-- -->
 count: 0,
 name: 'Leo'
})

watchEffect(() => {<!-- -->
 // execute immediately
 // Count: 0, Name: Leo
 console.log(`Count: ${<!-- -->state.count}, Name: ${<!-- -->state.name}`)
})

state.count + + // Count: 1, Name: Leo
state.name = 'alex' // Count: 1, Name: alex

In the above example, we use watchEffect to listen for changes to the status, count, and name properties. Whenever any of them changes the callback function records the current count and name values.

The first time watchEffect is called, the callback function is executed immediately, using the current count and name values. After this, the callback function will be re-run when any react dependency (count or name) changes.

Just like watch, watchEffect has some additional features that make it more powerful. We can pass an options object as the second parameter to configure the observer behavior. For example, you can specify flush timing (when the observer is executed) or add a debug hook.

When the first parameter of the callback function is the special function onCleanup, you can use this function to register a cleanup callback that will be called before the monitor is re-executed. This helps clean up resources that are no longer needed.

import {<!-- --> ref, watchEffect } from "vue"

const id = ref(1)
const data = ref(null)

watchEffect(async (onCleanup) => {<!-- -->
 const {<!-- --> response, cancel } = await fetch(`https://example.com/api/data/${<!-- -->id.value}`)
 onCleanup(cancel)
 data.value = response.data
})

In the above example, we use watchEffect to get data from API when the ID property changes. We use the onCleanup function to register a cancellation function that will cancel the fetch request if the ID attribute changes before the request is completed.

In addition, we can use watchEffect to prevent monitoring.

import {<!-- --> watchEffect } from "vue"

const stop = watchEffect(() => {<!-- -->
 //…
})
// Stop the watcher
stop()

Another confusing issue is that watchEffect only tracks dependencies during synchronous execution. All properties accessed before the first awaited item will be tracked using an asynchronous callback, but all properties after that will not be tracked.

<script setup>
import {<!-- --> reactive, watchEffect } from "vue"

const state = reactive({<!-- -->
 count: 0,
 name: 'Leo'
})

const sleep = ms => new Promise(
  resolve => setTimeout(resolve, ms));

watchEffect(async () => {<!-- -->
 console.log(`Count: ${<!-- -->state.count}`) // This will be tracked
 await sleep(200)
 console.log(`Name: ${<!-- -->state.name}`) // This will NOT be tracked
})

  
</script>


<template>
<input type="number" v-model="state.count" />
<br />
<input v-model="state.name" />
</template>

Everything before await (i.e. state.count) will be tracked.
Everything after await (i.e. state.name) is not tracked.

Summary

watch and watchEffect both allow us to perform side effects in a reactive manner, but they differ in how dependencies are tracked:

  • watch only tracks explicit watch sources and does not track any access within callbacks. Additionally, the callback only fires when the source actually changes. Separating dependency tracking from side effects, watch provides more precise control when callbacks are run.
  • watchEffect combines dependency tracking and side effects into a single stage. During synchronous execution, it automatically tracks every reactive property accessed. This results in cleaner code, but makes its passive dependencies less explicit.

If we need to view several properties in a nested data structure, watchEffect() may be more efficient than the depth monitor, since it only tracks the properties used in the callback, rather than recursively tracking all properties.

But watch ensures better overall control, distinguishes dependency tracking from side effects, and avoids unexpected performance degradation.