React useEffect(): Fuga de memoria y función de limpieza

React useEffect(): Fuga de memoria y función de limpieza

Algunos efectos necesitan "limpiarse" después de que desmontamos un componente para evitar fugas de memoria.

Introducción

En mi artículo anterior, hemos visto cómo ejecutar efectos secundarios (o simplemente efectos). Sin embargo, alguno de los efectos crean recursos que necesitamos limpiar/sanear antes de remover/desmontar el componente de la IU, como por ejemplo peticiones a una API, un event listener en el DOM y una subscripción a un WebSocket.

Las fugas de memoria en las aplicaciones React son el resultado de no limpiar/cancelar las suscripciones realizadas en un componente antes de desmontarlo.

Ejemplo preliminar

Aquí disponemos de una aplicación que nos da la información de la anchura en pixeles de nuestra ventana.

Con el botón, alternamos entre mostrar dicha información o no.

App.js

import React from "react"
import WindowTracker from "./WindowTracker"

export default function App() {

    const [mostrar, setMostrar] = React.useState(true)

    function toggleMostrar() {
        setMostrar(estadoAnterior => !estadoAnterior)
    }

    return (
        <div className="container">
            <button onClick={toggleMostrar}>
                Toggle WindowTracker
            </button>
            {mostrar && <WindowTracker />}
        </div>
    )
}

WindowTracker.js

import React from "react"

export default function WindowTracker() {
    return (
        <h1>Window width: {window.innerWidth}</h1>
    )
}

reside window.gif Si ajustamos el tamaño de la pantalla y conmutamos el estado haciendo clic en el botón, veremos que la información cambia; esto se debe a que cuando desactivamos el componente WindowTracker éste es "desmontado" o "removido" del DOM.

Cuando lo activamos, lo volvemos a "montar" o "añadir" al DOM, por lo que volverá a ejecutar {window.innerWidth}, lo que nos da la información actualizada sobre la anchura de la ventana.

Añadiendo un event listener

Ahora vamos a añadir un event listener a window, que escuchará el evento resize,por lo que cada vez que la ventana es redimensionada,nos mostrará información en tiempo real de la anchura de nuestra ventana:

import React from "react"

export default function WindowTracker() {
    const [anchura, setAnchura] = React.useState(window.innerWidth);

    React.useEffect(() => {
        window.addEventListener('resize', (e) => {
            setAnchura(e.target.innerWidth);
        })
    }, [])

    return (
        <h1>Window width: {anchura}</h1>
    )
}

resize dinamically.gif

Ahora la información es actualizada de forma dinámica.

Observemos un bug que puede ocurrir: Ahora mismo, nuestro componente <WindowTracker/> sólo se añade al DOM cuando nuestra variable de estado mostrar (dentro de App.js) es true.

Cuando se renderiza el componente, se ejecuta el manejador de eventos/event listener cada vez que se redimensiona la ventana, dicho manejador de eventos/event listener está registrado en el DOM, por lo que si removemos el componente, e intentamos redimensionar la ventana, React nos muestra un error de que no podemos actualizar el estado de un componente que ha sido removido.

memory leak.png

Esto ocurre porque <WindowTracker/> ya no es parte del DOM, pero el browser continua escuchando por el evento resize y continua intentando fijar el estado del componente (el cual ha sido removido) con setWindowWidth(window.innerWidth).

Este tipo de bug se llama fuga de memoria.

Usando la función de limpieza de efectos

Una de las cosas que debemos prestar atención a la hora de trabajar con efectos, es a las potenciales consecuencias que podemos tener si no limpiamos el efecto.

Si nos damos cuenta, useEffect() recibe una función callback como primer parámetro, pero ahora mismo no estamos retornando nada desde esas función, por lo que debemos retornar una función de limpieza.

Como useEffect() no sabe cuál fue el efecto que hemos ejecutado, nosotros somos los que debemos decidir cómo limpiar el efecto, escribiendo el código correspondiente en el cuerpo de la función retornada.

En este caso, deberíamos usar removeEventListener() alque le pasamos como segundo argumento la misma función que a addEventListener():

import React from "react"

export default function WindowTracker() {
    const [anchura, setAnchura] = React.useState(window.innerWidth);

    React.useEffect(() => {
        function mostrarRedimesion(e){
            setAnchura(e.target.innerWidth);
        }

        window.addEventListener('resize',mostrarRedimension);

        return function(){
            console.log("El event listener ha sido limpiado.")
            window.removeEventListener('resize',mostrarRedimension);
        }

    }, [])

    return (
        <h1>Window width: {anchura}</h1>
    )
}

cleanup function.gif