Watchers
Ejemplo Básico
Las propiedades computadas nos permiten calcular valores derivados de forma declarativa. Sin embargo, hay casos en los que necesitamos realizar "efectos secundarios" en reacción a cambios de estado, por ejemplo, mutar el DOM o cambiar otra parte del estado basándose en el resultado de una operación asíncrona.
Con la API de Composición, podemos usar la función watch para activar un callback siempre que una parte del estado reactivo cambie:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref(
'Las preguntas suelen contener un signo de interrogación. ;-)'
)
const loading = ref(false)
// watch funciona directamente en una refwatch funciona directamente en una ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Pensando...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = '¡Error! No se pudo acceder a la API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Haz una pregunta de sí/no:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>Tipos de Fuentes de Watch
El primer argumento de watch puede ser de diferentes tipos de "fuentes" reactivas: puede ser un ref (incluyendo refs computadas), un objeto reactivo, una función getter, o un array de múltiples fuentes:
js
const x = ref(0)
const y = ref(0)
// ref única
watch(x, (newX) => {
console.log(`x es ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`la suma de x + y es: ${sum}`)
}
)
// array de múltiples fuentes
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x es ${newX} y y es ${newY}`)
})Ten en cuenta que no puedes observar una propiedad de un objeto reactivo de esta manera:
js
const obj = reactive({ count: 0 })
// esto no funcionará porque estamos pasando un número a watch()
watch(obj.count, (count) => {
console.log(`La Cuenta es: ${count}`)
})En su lugar, usa un getter:
js
// en su lugar, usa un getter:
watch(
() => obj.count,
(count) => {
console.log(`La Cuenta es: ${count}`)
}
)Watchers Profundos
Cuando llamas a watch() directamente sobre un objeto reactivo, creará implícitamente un watcher profundo - la función de callback se activará en todas las mutaciones anidadas:
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// se dispara en mutaciones de propiedades anidadas
// Nota: `newValue` será igual a `oldValue` aquí
// ¡porque ambos apuntan al mismo objeto!
})
obj.count++Esto debe diferenciarse de un getter que devuelve un objeto reactivo; en este último caso, la función de callback solo se disparará si el getter devuelve un objeto diferente:
js
watch(
() => state.someObject,
() => {
// se dispara solo cuando state.someObject es reemplazado
}
)Sin embargo, puedes forzar el segundo caso a ser un watcher profundo usando explícitamente la opción deep:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Nota: `newValue` será igual a `oldValue` aquí
// *a menos que* state.someObject haya sido reemplazado
},
{ deep: true }
)En Vue 3.5+, la opción deep también puede ser un número que indica la profundidad máxima de recorrido, es decir, cuántos niveles debe recorrer Vue las propiedades anidadas de un objeto.
Úsalo con Precaución
La observación profunda requiere recorrer todas las propiedades anidadas en el objeto observado, y puede ser costosa cuando se usa en grandes estructuras de datos. Úsala solo cuando sea necesario y ten en cuenta las implicaciones de rendimiento.
Watchers de Ejecución Inmediata
watch es perezoso por defecto: la función de callback no se llamará hasta que la fuente observada haya cambiado. Pero en algunos casos es posible que queramos que la misma lógica de callback se ejecute de forma inmediata - por ejemplo, es posible que queramos obtener algunos datos iniciales y luego volver a obtener los datos cada vez que los estados relevantes cambien.
Podemos forzar la ejecución inmediata de la función de callback de un watcher pasando la opción immediate: true:
js
watch(
source,
(newValue, oldValue) => {
// ejecutado inmediatamente, luego de nuevo cuando `source` cambie
},
{ immediate: true }
)Watchers de Una Vez
- Solo soportado en 3.4+
La función de callback de un watcher se ejecutará siempre que la fuente observada cambie. Si quieres que la función de callback se active solo una vez cuando la fuente cambie, usa la opción once: true.
js
watch(
source,
(newValue, oldValue) => {
// cuando `source` cambia, se activa solo una vez
},
{ once: true }
)watchEffect()
Es común que la función de callback del watcher use exactamente el mismo estado reactivo que la fuente. Por ejemplo, considera el siguiente código, que usa un watcher para cargar un recurso remoto cada vez que el ref todoId cambia:
js
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)En particular, fíjate cómo el watcher usa todoId dos veces, una como fuente y otra dentro del callback.
Esto se puede simplificar con watchEffect(). watchEffect() nos permite rastrear automáticamente las dependencias reactivas de la función de callback. El watcher anterior se puede reescribir como:
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})Aquí, la función de callback se ejecutará inmediatamente, no hay necesidad de especificar immediate: true. Durante su ejecución, rastreará automáticamente todoId.value como una dependencia (similar a las propiedades computadas). Cada vez que todoId.value cambie, la función de callback se ejecutará de nuevo. Con watchEffect(), ya no necesitamos pasar todoId explícitamente como el valor de la fuente.
Puedes ver este ejemplo de watchEffect() y la obtención de datos reactivos en acción.
Para ejemplos como estos, con una sola dependencia, el beneficio de watchEffect() es relativamente pequeño. Pero para watchers que tienen múltiples dependencias, usar watchEffect() elimina la carga de tener que mantener la lista de dependencias manualmente. Además, si necesitas observar varias propiedades en una estructura de datos anidada, watchEffect() puede resultar más eficiente que un watcher profundo, ya que solo rastreará las propiedades que se usan en la función de callback, en lugar de rastrearlas todas recursivamente.
TIP
watchEffect solo rastrea dependencias durante su ejecución síncrona. Cuando se usa con una función de callback asíncrona, solo se rastrearán las propiedades a las que se accede antes del primer await.
watch vs. watchEffect
watch y watchEffect nos permiten realizar efectos secundarios de forma reactiva. Su principal diferencia radica en la forma en que rastrean sus dependencias reactivas:
watchsolo rastrea la fuente observada explícitamente. No rastreará nada a lo que se acceda dentro de la función de callback. Además, la función de callback solo se activa cuando la fuente ha cambiado realmente.watchsepara el rastreo de dependencias del efecto secundario, dándonos un control más preciso sobre cuándo debe dispararse la función de callback.watchEffect, por otro lado, combina el rastreo de dependencias y el efecto secundario en una sola fase. Rastrea automáticamente cada propiedad reactiva a la que se accede durante su ejecución síncrona. Esto es más conveniente y generalmente resulta en un código más conciso, pero hace que sus dependencias reactivas sean menos explícitas.
Limpieza de Efectos Secundarios
A veces podemos realizar efectos secundarios, por ejemplo, peticiones asíncronas, en un watcher:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// lógica del callback
})
})Pero, ¿qué pasa si id cambia antes de que se complete la petición? Cuando la petición anterior se complete, seguirá activando la función de callback con un valor de ID que ya está obsoleto. Idealmente, queremos poder cancelar la petición obsoleta cuando id cambie a un nuevo valor.
Podemos usar la API onWatcherCleanup() para registrar una función de limpieza que se llamará cuando el watcher se invalide y esté a punto de volver a ejecutarse:
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// lógica del callback
})
onWatcherCleanup(() => {
// abortar petición obsoleta
controller.abort()
})
})Ten en cuenta que onWatcherCleanup solo es compatible con Vue 3.5+ y debe llamarse durante la ejecución síncrona de una función de efecto watchEffect o una función de callback watch: no puedes llamarla después de una sentencia await en una función asíncrona.
Alternativamente, una función onCleanup también se pasa a las funciones de callback del watcher como tercer argumento, y a la función de efecto watchEffect como primer argumento:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// lógica de limpieza
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// lógica de limpieza
})
})Esto funciona en versiones anteriores a la 3.5. Además, onCleanup pasado como argumento de función está vinculado a la instancia del watcher, por lo que no está sujeto a la restricción síncrona de onWatcherCleanup.
Momento de Vaciado de la Función de Callback
Cuando mutas el estado reactivo, esto puede activar tanto las actualizaciones de los componentes de Vue como las funciones de callback de los watchers creados por ti.
De forma similar a las actualizaciones de componentes, las funciones de callback de los watchers creados por el usuario se agrupan para evitar invocaciones duplicadas. Por ejemplo, probablemente no queremos que un watcher se active mil veces si insertamos mil elementos de forma síncrona en un array que está siendo observado.
Por defecto, la función de callback de un watcher se llama después de las actualizaciones del componente padre (si las hay), y antes de las actualizaciones del DOM del componente propietario. Esto significa que si intentas acceder al propio DOM del componente propietario dentro de una función de callback de un watcher, el DOM estará en un estado de pre-actualización.
Watchers Post-Vaciado
Si quieres acceder al DOM del componente propietario en una función de callback de un watcher después de que Vue lo haya actualizado, necesitas especificar la opción flush: 'post':`
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})El watchEffect() de post-vaciado también tiene un alias de conveniencia, watchPostEffect():
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* ejecutado después de que Vue actualice */
})Watchers Síncronos
También es posible crear un watcher que se dispara de forma síncrona, antes de cualquier actualización gestionada por Vue:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})El watchEffect() síncrono también tiene un alias de conveniencia, watchSyncEffect():
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* ejecutado sincrónicamente al cambiar los datos reactivos */
})Úsalo con Precaución
Los watchers síncronos no tienen agrupación y se activan cada vez que se detecta una mutación reactiva. Está bien usarlos para observar valores booleanos simples, pero evita usarlos en fuentes de datos que puedan mutarse sincrónicamente muchas veces, por ejemplo, arrays.
Deteniendo un Watcher
Los watchers declarados de forma síncrona dentro de setup() o <script setup> están vinculados a la instancia del componente propietario, y se detendrán automáticamente cuando el componente propietario se desmonte. En la mayoría de los casos, no necesitas preocuparte por detener el watcher tú mismo.
La clave aquí es que el watcher debe crearse de forma síncrona: si el watcher se crea en una función de callback asíncrona, no se vinculará al componente propietario y deberá detenerse manualmente para evitar fugas de memoria. Aquí tienes un ejemplo:
vue
<script setup>
import { watchEffect } from 'vue'
// este se detendrá automáticamente
watchEffect(() => {})
// ...¡este no lo hará!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>Para detener manualmente un watcher, usa la función de manejo devuelta. Esto funciona tanto para watch como para watchEffect:
js
const unwatch = watchEffect(() => {})
// ...más tarde, cuando ya no sea necesario
unwatch()Ten en cuenta que debería haber muy pocos casos en los que necesites crear watchers de forma asíncrona, y la creación síncrona debe preferirse siempre que sea posible. Si necesitas esperar algunos datos asíncronos, puedes hacer que tu lógica de observación sea condicional en su lugar:
js
// datos a cargar de forma asíncrona
const data = ref(null)
watchEffect(() => {
if (data.value) {
// hacer algo cuando los datos estén cargados
}
})