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) que useEffect ejecuta después de dibujar y limpia antes de la siguiente ejecución y cuando el componente desaparece.

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.

React

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.

El cuerpo de la función StockWatcher es el dibujo. Se ejecuta en cada dibujo y, en el ejemplo de arriba, inicia un temporizador nuevo cada vez. Aquí no se nota, porque el banner se dibuja una sola vez. Pero si el temporizador cambia el estado (por ejemplo, un contador de comprobaciones en la pantalla), cada cambio dibuja de nuevo y cada dibujo inicia un temporizador más. Su número crece cada segundo.

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.

import { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
const id = setInterval(check, 1000);
return () => clearInterval(id);
}, [product]);
return <p role="status">Vigilamos el stock de {product.name}.</p>;
}
  • useEffect(función, dependencias) le dice a React: «ejecuta esta función cuando los cambios ya estén en el DOM».
  • La función que el efecto devuelve es la limpieza. React la llama cuando el componente sale de la página.
  • El segundo argumento es la lista de dependencias: los valores que lee el efecto. La siguiente idea trata de ella.

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.

React

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 la lista de dependencias. Después de cada dibujo, React compara cada valor de la lista con el de la vez anterior. Si al menos uno es distinto, primero llama a la limpieza de la ejecución antigua y luego ejecuta el efecto otra vez.

El segundo argumento Cuándo se ejecuta el efecto
[product] después del primer dibujo y después de cada dibujo en que product sea distinto
[] solo después del primer dibujo
ninguno después de cada dibujo

La regla: la lista contiene cada valor de props y estado que el efecto lee. Si falta product, el efecto se queda con el producto antiguo. La función del efecto se creó en el primer dibujo y recuerda los valores de entonces.

Míralo

La vida de un efecto

El banner se abre para la banitsa, el cliente elige los pepinos y luego cierra el banner.

function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Comprobación n.º ${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return <p role="status">Vigilamos el stock de {product.name}</p>;
}
Paso a paso, en palabras
  1. Dibujo: React llama a StockWatcher y recibe el marcado. La función solo calcula qué mostrar. No inicia nada.
  2. El componente está en la página. Ahora React ejecuta el efecto: el temporizador arranca y cuenta las comprobaciones.
  3. El cliente elige otro producto. La prop cambia, StockWatcher se dibuja de nuevo y React compara [product] con la vez anterior: es distinto.
  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 cliente cambia el producto, pero todas las comprobaciones son del primero. ¿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?

useEffect(() => {
  const id = setInterval(check, 1000);
   () => clearInterval(id);
}, [product]);

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 (o con useMemo), 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

La doble ejecución en desarrollo. En un proyecto nuevo de Vite, main.jsx envuelve <App /> en <StrictMode>. En desarrollo, StrictMode ejecuta adrede cada efecto, lo limpia y lo ejecuta otra vez, justo al mostrarse. Si la consola dice dos veces «el efecto arrancó», es una prueba, no un error: un efecto con una limpieza correcta la supera. El sandbox no usa StrictMode, así que aquí el efecto arranca una vez.

Por qué no useEffect(async () => …). Una función async devuelve una promesa, y React espera una función de limpieza o nada. La consola dice useEffect must not return anything besides a function, which is used for clean-up. La función async se escribe dentro del efecto y se llama allí. Eso viene en la siguiente lección.

Cómo se comparan las dependencias. Con Object.is, como con el estado. Un objeto creado de nuevo en cada dibujo es distinto cada vez, y el efecto se ejecuta cada vez. La regla «todo lo que lee el efecto está en la lista» la vigila el linter (react-hooks/exhaustive-deps), que forma parte de la plantilla de Vite.

Los componentes de clase tienen lo mismo en tres métodos: componentDidMount, componentDidUpdate y componentWillUnmount.

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.

React

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 efecto lee product y su lista de dependencias es []. El cliente cambia el producto. ¿Qué pasa?
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 ·