Efectos
En resumen: Un efecto es código para acciones secundarias (un temporizador, una petición, el título de la pestaña); en Vue, onMounted lo ejecuta cuando el componente ya está en la página, watch lo ejecuta cuando cambia su fuente, y la limpieza detiene lo que se inició.
Problema
El banner ya no está, las comprobaciones sí
La banitsa de queso está agotada. El cliente pulsa «Vigilar el stock» y debajo del botón aparece un banner. Mientras el banner está abierto, la página comprueba el stock cada segundo. Aquí la comprobación solo escribe una línea en la consola.
Consola
Pulsa «Vigilar el stock», espera dos segundos y pulsa «Dejar de vigilar». El banner desaparece, pero «Comprobación n.º 3», «Comprobación n.º 4»… siguen llegando a la consola. Abre y cierra el banner otra vez: ahora hay dos líneas por segundo. El temporizador pertenece al navegador, no al componente. Nadie lo detuvo.
La idea
Dibujar solo calcula
Un componente dice qué debe haber en la pantalla: a partir de las props y el estado calcula el marcado. Eso es dibujar. La biblioteca lo repite cuando quiere y compara el resultado con el anterior.
Por eso dibujar tiene que ser puro: no hace nada salvo calcular el marcado. Un temporizador, una petición al servidor, un cambio de document.title, una suscripción a eventos de window hacen algo hacia fuera. Son efectos secundarios.
En <script setup> el código se ejecuta una vez, cuando se crea el componente. Por eso el temporizador del ejemplo arranca una vez por apertura. Pero arranca mientras el componente aún se está creando, y nada lo detiene cuando el componente desaparece. La plantilla es el dibujo: Vue la dibuja de nuevo cada vez que cambian los datos que lee. También ella debe limitarse a describir lo que se muestra.
Un efecto secundario necesita dos cosas que el dibujo no da: un momento para empezar y un momento para parar.
La idea
Efecto y limpieza
Un efecto es código que el componente ejecuta cuando ya está en la página. La limpieza es el código que detiene lo que se inició cuando el componente desaparece.
<script setup>import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['product']);
let id;onMounted(() => { id = setInterval(check, 1000);});onUnmounted(() => clearInterval(id));</script>onMounted(función)le dice a Vue: «ejecútala cuando el componente esté en la página».onUnmounted(función)se llama cuando el componente sale de la página. Esa es la limpieza.idvive fuera de las dos funciones, porque las dos necesitan verlo.
La limpieza es el reflejo del efecto: setInterval y clearInterval, addEventListener y removeEventListener, una petición y la cancelación de esa petición.
Pruébalo
Detén las comprobaciones
El temporizador de StockWatcher arranca directamente en el componente y nadie lo detiene. Muévelo a un efecto que arranque cuando aparezca el banner y deténlo cuando el banner desaparezca. La comprobación abre el banner, espera «Comprobación n.º 1», lo cierra y se asegura de que «Comprobación n.º 2» nunca llegue.
Consola
Solución
App.vue<script setup>
import { ref } from 'vue';
import StockWatcher from './StockWatcher.vue';
const PRODUCT = { id: 4, name: 'Banitsa de queso' };
const watching = ref(false);
</script>
<template>
<main>
<h1>{{ PRODUCT.name }}</h1>
<p>Agotado. Volverá pronto.</p>
<button type="button" @click="watching = !watching">
{{ watching ? 'Dejar de vigilar' : 'Vigilar el stock' }}
</button>
<StockWatcher v-if="watching" :product="PRODUCT" />
</main>
</template>
StockWatcher.vue<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['product']);
let id;
onMounted(() => {
let checks = 0;
id = setInterval(() => {
checks++;
console.log(`Comprobación n.º ${checks}: ${props.product.name}`);
}, 1000);
});
onUnmounted(() => clearInterval(id));
</script>
<template>
<p class="watcher" role="status">
Vigilamos el stock de {{ product.name }}. Te avisaremos en cuanto vuelva.
</p>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
El temporizador va en onMounted, y onUnmounted(() => clearInterval(id)) lo detiene. id es un let fuera de las dos funciones. La comprobación detecta const id = setInterval(…) dentro de onMounted: ese es un id nuevo, visible solo ahí. El de fuera queda undefined, y clearInterval(undefined) no detiene nada.
La idea
Dependencias
Ahora el cliente puede elegir otro producto agotado mientras el banner está abierto. El efecto comprueba product. En cuanto el producto cambia, la comprobación antigua tiene que detenerse y una nueva tiene que empezar para el producto nuevo.
Para eso sirve watch. Vigila una fuente y llama a su función cuando la fuente cambia:
watch(() => props.product, (product, oldProduct, onCleanup) => { const id = setInterval(check, 1000); onCleanup(() => clearInterval(id));}, { immediate: true });- La fuente es una función que devuelve el valor:
() => props.product. Vue ve lo que lee y sabe cuándo volver a llamarla. { immediate: true }ejecuta también la función de inmediato, para el producto inicial. Sin ella,watchespera al primer cambio.onCleanupregistra la limpieza de esta ejecución. Vue la llama antes de la siguiente ejecución y cuando el componente desaparece. Los watchers de<script setup>se detienen solos con el componente.
watch sustituye a las dos funciones de la última tarea: onMounted se ejecuta una vez, mientras que watch se ejecuta con cada producto nuevo.
Míralo
La vida de un efecto
El banner se abre para la banitsa, el cliente elige los pepinos y luego cierra el banner.
<script setup>const props = defineProps(['product']);
watch(() => props.product, (product, old, onCleanup) => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Comprobación n.º ${checks}: ${product.name}`); }, 1000); onCleanup(() => clearInterval(id));}, { immediate: true });</script>
<template> <p role="status">Vigilamos el stock de {{ product.name }}</p></template>Paso a paso, en palabras
- Dibujo: Vue construye el marcado a partir de la plantilla. La plantilla solo describe qué mostrar. No inicia nada.
- immediate: true ejecuta el watcher una vez de inmediato, al crearse el componente. El temporizador arranca y cuenta las comprobaciones.
- El cliente elige otro producto. La función () => props.product ahora devuelve otro valor, así que el watcher tiene que ejecutarse otra vez.
- Antes de la nueva ejecución llega la limpieza de la anterior: su temporizador se detiene. Sin ella, las comprobaciones antiguas seguirían junto a las nuevas.
- El efecto se ejecuta de nuevo con el producto nuevo. Su propio temporizador cuenta desde el n.º 1.
- El cliente cierra el banner: el componente sale de la página. Se ejecuta la última limpieza y el temporizador se detiene con ella.
- Cada ejecución tiene su pareja: lo que el efecto inicia, su limpieza lo detiene. Antes de la siguiente ejecución y cuando el componente se va.
El efecto se ejecuta dos veces y la limpieza se ejecuta dos veces: una antes de la nueva ejecución y otra cuando se retira el componente. Entre medias hay siempre exactamente un temporizador.
Pruébalo
Encuentra el error
TypeError: Cannot read properties of undefined (reading 'name'). ¿Qué línea tiene la culpa?Elige la línea con el error.
watch(props.product, …) le da a watch el producto actual, no una fuente. Vue no puede vigilar un objeto simple y dice [Vue warn]: Invalid watch source. Por immediate, la función se ejecuta una vez, pero con undefined en lugar de un producto, y el temporizador lanza un error cada segundo. Hace falta una función: watch(() => props.product, …).Pruébalo
Completa
¿Qué va en el hueco para que el temporizador se detenga cuando el componente desaparece?
let id;
onMounted(() => {
id = setInterval(check, 1000);
});
(() => clearInterval(id));onUnmounted: Vue llama a la función cuando el componente sale de la página.La idea
Cuándo no necesitas un efecto
Un efecto sirve para enlazar con algo de fuera del componente: un temporizador, un servidor, el título de la pestaña, window. Dos cosas parecen un efecto, pero no lo son.
- Un valor calculado a partir de otros. El total del carrito no se guarda en un estado aparte que un efecto actualiza. Se calcula al dibujar con
computed, como en la lección sobre estado derivado. Un efecto que lo escribe en un estado aparte es trabajo de más y un segundo lugar que puede desviarse del carrito. - Una respuesta a una acción. El pedido se envía porque el cliente pulsó «Pedir». Eso va en el manejador del clic, no en un efecto que vigila alguna bandera.
Lo que queda es el verdadero trabajo de un efecto: mantener el componente sincronizado con el mundo exterior mientras está en la página.
Más precisamente, para curiosos
watchEffect. watchEffect((onCleanup) => { … }) arranca de inmediato y vigila todo lo que lee su función. No enumeras las fuentes, pero tampoco ves de un vistazo qué lo dispara. watch con una fuente explícita es más claro cuando necesitas saber exactamente cuándo se ejecuta.
onWatcherCleanup. Desde Vue 3.5 la limpieza también se puede registrar con onWatcherCleanup(() => …), importado de 'vue'. Hay que llamarlo antes del primer await de la función.
Cuándo se ejecuta immediate. Una función con { immediate: true } se ejecuta mientras se crea el componente, antes de que esté en el DOM. Un temporizador y una petición no necesitan el DOM. Si el efecto tiene que tocar elementos de la plantilla, el sitio es onMounted.
La Options API tiene lo mismo: mounted, unmounted y la opción watch.
Ejercicio
Otro producto
Encima del banner hay un menú desplegable «Producto» con la banitsa y los pepinos. Ahora mismo StockWatcher inicia el temporizador una vez, cuando se muestra. Haz que, al cambiar el producto, la comprobación antigua se detenga y una nueva empiece desde el n.º 1 para el producto nuevo. La comprobación abre el banner, espera «Comprobación n.º 1» de la banitsa, elige los pepinos, espera «Comprobación n.º 1» de ellos, se asegura de que «Comprobación n.º 2» de la banitsa nunca llegue y cierra el banner.
Consola
Solución
App.vue<script setup>
import { ref, computed } from 'vue';
import StockWatcher from './StockWatcher.vue';
const PRODUCTS = [
{ id: 4, name: 'Banitsa de queso' },
{ id: 13, name: 'Pepinos' },
];
const productId = ref(4);
const watching = ref(false);
const product = computed(() => PRODUCTS.find((p) => p.id === productId.value));
</script>
<template>
<main>
<label>
Producto
<select :value="productId" @change="productId = Number($event.target.value)">
<option v-for="p in PRODUCTS" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</label>
<h1>{{ product.name }}</h1>
<p>Agotado. Volverá pronto.</p>
<button type="button" @click="watching = !watching">
{{ watching ? 'Dejar de vigilar' : 'Vigilar el stock' }}
</button>
<StockWatcher v-if="watching" :product="product" />
</main>
</template>
StockWatcher.vue<script setup>
import { watch } from 'vue';
const props = defineProps(['product']);
watch(() => props.product, (product, oldProduct, onCleanup) => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Comprobación n.º ${checks}: ${product.name}`);
}, 1000);
onCleanup(() => clearInterval(id));
}, { immediate: true });
</script>
<template>
<p class="watcher" role="status">
Vigilamos el stock de {{ product.name }}. Te avisaremos en cuanto vuelva.
</p>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
onMounted y onUnmounted se sustituyen por watch(() => props.product, (product, oldProduct, onCleanup) => { … }, { immediate: true }), y el temporizador se detiene con onCleanup. La comprobación detecta watch sin immediate (la primera comprobación nunca llega), watch(props.product, …) (una fuente no válida) y watch sin onCleanup (los dos temporizadores van juntos).
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Props: datos desde arriba · Condiciones: vacío, error, cargando · Estado · Estado derivado
- Continúa en: Datos de una API en un componente · Velocidad: cuándo se vuelve a renderizar un componente · Probar componentes · Del sandbox al proyecto
- Ejercicios sobre el tema: Efectos, datos y formularios