Ir al contenido
wannacode

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.

Vue

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.
  • id vive 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.

Vue

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, watch espera al primer cambio.
  • onCleanup registra 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
  1. Dibujo: Vue construye el marcado a partir de la plantilla. La plantilla solo describe qué mostrar. No inicia nada.
  2. immediate: true ejecuta el watcher una vez de inmediato, al crearse el componente. El temporizador arranca y cuenta las comprobaciones.
  3. El cliente elige otro producto. La función () => props.product ahora devuelve otro valor, así que el watcher tiene que ejecutarse otra vez.
  4. 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.
  5. El efecto se ejecuta de nuevo con el producto nuevo. Su propio temporizador cuenta desde el n.º 1.
  6. El cliente cierra el banner: el componente sale de la página. Se ejecuta la última limpieza y el temporizador se detiene con ella.
  7. 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

El banner se abre, pero en lugar de comprobaciones la consola muestra una advertencia y, cada segundo, TypeError: Cannot read properties of undefined (reading 'name'). ¿Qué línea tiene la culpa?

Elige la línea con el error.

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));

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.

Vue

Test

Ponte a prueba

1. Un componente inicia setInterval y nunca lo detiene. El cliente abre y cierra el banner tres veces. ¿Cuántos temporizadores están en marcha?
2. El watcher es watch(() => props.product, …), sin { immediate: true }. ¿Cuándo empieza la primera comprobación?
3. ¿Cuándo se ejecuta la limpieza de un efecto?
4. ¿Cuál de estas cosas NO es trabajo para un efecto?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·