Cómo (y porqué) usar la limpieza de useEffect en React: Intervalos

Usar setInterval con useEffect puede ser confuso, puede suceder que se pisen los intervalos, o que se usen variables con referencias que ya no existen. Podemos evitar esto usando correctamente la función de limpieza de useEffect.

Cómo (y porqué) usar la limpieza de useEffect en React: Intervalos

Usar setInterval con useEffect puede ser confuso, puede suceder que se pisen los intervalos, o que se usen variables con referencias que ya no existen. Podemos evitar esto usando correctamente la función de limpieza de useEffect.

If want to see this post in English, click here.

¿Qué pasa si no limpiamos los intervalos?

Supongamos que tenemos un componente que muestra texto que parpadea, como en los viejos tiempos.

const Blinker = ({ text }) => {
  const [visible, setVisible] = useState(true);
  useEffect(() => {
    setInterval(() => {
      console.log(`El texto parpadeando es: ${text}`);
      setVisible((visible) => !visible);
    }, 1000);
  }, [text]);
  return visible ? <h1>{text}</h1> : null;
};
Nota: La dependencia del useEffect [text] esta causando que el hook sea ejecutado de vuelta cada vez que cambia el texto. La única razón que tuvimos para agregar esta dependencia es que el console.log funcione correctamente, como podríamos considerar que no es vital para nuestra aplicación, podríamos sacarlo.

Pero considerando que hay muchas razones válidas para añadir un prop al array de dependencias, asumamos que es un requerimiento fuerte.

¿Qué pasa con Blinker cuando renderiza por primera vez?

  1. El efecto corre por primera vez, ya que los efectos siempre corren la primera vez que se renderiza (montado)
  2. El primer intervalo arranca, lo cual imprime por la consola la string "El texto parpadeando es: " cada segundo.
  3. El componente retorna un elemento header vacío, que el browser renderiza.

¿Qué pasa con Blinker cuando el prop text cambia a “a”?

  1. El componente Blinker renderiza de vuelta, ya que cuando cambian los props o el estado, react va a renderizar nuestro componente nuevamente.
  2. React revisa las dependencias del useEffect, y como cambio una (text0), ejecuta la función del efecto nuevamente.
  3. Un nuevo intervalo arranca, el cual imprime por la consola Current blinking text: a
  4. El componente retorna un elemento header con la letra “a”, que se puede ver en la pantalla.

En este escenario, la consola del navegador se ve así:

browser's console screenshot

Dos intervalos están corriendo al mismo tiempo, cada uno imprimiendo algo distinto. Esto pasa porque no borramos el intervalo viejo, ¡así que nunca dejó de imprimir!

Solución

Para solucionar esto, podemos usar la función de limpieza de useEffect, que se ve así:

const Blinker = ({ text }) => {
  const [visible, setVisible] = useState(true);
  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(`El texto parpadeando es: ${text}`);
      setVisible((visible) => !visible);
    }, 1000);
    return () => {
      clearInterval(intervalId);
    };
  }, [text]);
  return visible ? <h1>{text}</h1> : null;
};

¿Qué pasa ahora cuando cambiamos el texto a “a”?

  1. El componente Blinker renderiza de vuelta, igual que antes, porque las props cambiaron.
  2. React revisa las dependencias de useEffect, y como son distintas, ejecuta la función de vuelta.
  3. Pero antes de ejecutarlo, va a correr la función que retornamos, limpiando el efecto anterior y borrando el viejo intervalo
  4. Un nuevo intervalo es registrado, que va a imprimir The text currently blinking is: a cada segundo.
  5. El componente retorna un elemento header con la letra “a”, que se puede ver en la pantalla.

Esto evita que los 2 intervalos se pisen, y hace que nuestro código se comporte como esperábamos. Es importante recordar que react va a ejecutar la función de limpieza antes de la próxima ejecución de nuestro hook, y cuando desmontemos el componente. Básicamente limpiamos antes de reaccionar a los cambios, y cuando ya no necesitamos el componente.

Pueden ver ambos ejemplos en este codesandbox, recuerden cambiar el componente exportado para ver ambos comportamientos detallados en este artículo.

Hay muchos otros escenarios donde la función de limpieza de useEffect es útil o necesaria.