Las dependencias de useEffect

Las dependencias de useEffect

Como definir correctamente las dependencias cuando estamos trabajando con useEffect.

useEffect es uno de los hooks que son más utilizados por los desarrolladores de React en su día a día pero es cierto que muchas veces no lo hacemos de la forma correcta lo que al final se puede traducir en problemas que son difíciles de encontrar, debuggear e incluso en problemas de rendimiento.

Vamos a ver uno de ellos y para ello nos vamos a basar en el código de un componente muy sencillo:

import { useEffect } from 'react'
import { trackEvent } from '@/utils'

export const Page = () => {
  const analyticsData = { userId: 1 }

  useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [])

  return <div></div>
}

Dentro componente Page lo que estamos haciendo es definir la variable analyticsData que no es más que un objeto con un único atributo denominado userId con el valor 1. La idea es que vamos a usar este objeto analyticsData dentro del useEffect y más concretamente como parámetro de una función trackEvent que servirá para recoger un evento del tipo pageEvent con los datos recogidos en este objeto.

Nota: con el fin de mantener el código lo más sencillo posible no vamos a mostrar la implementación de la función trackEvent puesto que no es algo relevante para la explicación que vamos a seguir.

En React existe una regla no escrita que viene a decir algo como que cada vez que definimos una variable dentro de uno de nuestros componentes (como estamos haciendo en el ejemplo a la hora de declarar analyticsData) y además el valor que almacena no es una valor primitivo (es decir, no es un boolean, string o number) y siempre que la vayamos a usar dentro de un useEffect deberemos establecerla dentro del array de dependencias que está asociado a ese efecto.

Vale, si hacemos esto el código de nuestro efecto quedará como sigue:

useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [analyticsData])

lo que al final vamos a tener es que este useEffect se va a ejecutar en todos y cada uno de los re-renders que se lleven a cabo sobre el componente Page puesto que el valor de analyticsData (que en este caso no es primitivo puesto que almacena un objeto) va a ser diferente en cada uno de estos renders lo que provoca que el array de dependencias de useEffect cambie y por lo tanto que el código que tiene en su interior se ejecute una vez más.

El problema podría ser todavía mucho peor en el caso de que dentro del código que estuviese asociado a la función contenida en useEffect supusiese, por ejemplo, el cambio en el valor de una variable de estado que provocase a su vez un nuevo re-render y por lo tanto acabaríamos cayendo en un bucle infinito de renderizados.

Así pues parece teniendo en cuenta la regla no escrita de React que todas las variables que vayamos a utilizar dentro de un useEffect tienen que estar en su array de dependencias parece ser que ante escenarios como el que acabamos de describir va a ser realmente complicado lograr el objetivo que estamos persiguiendo.

¿Qué solución se suele adoptar en estos casos? Pues la mayoría de los desarrolladores por lo optan es quitar la variables del array de depencias (es decir, no seguir la regla) y dejar el código que teníamos en un principio.

useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [])

De esta manera este useEffect tenemos la garantía de que únicamente va a ser ejecutado una vez (en concreto cuando el componente se acaba de montar en el DOM) puesto que el array de dependencias está vacío lo que supondrá que se ejecute la función trackEvent con los datos de analytics que están definidos en el objeto analyticsData.

Problema con ESLint

Ahora bien, es más que probable que en el proyecto en el que estemos trabajando tengamos habilitado ESLint lo que hará que en nuestro editor nos avise de que hay un warning en el código con el que estamos definiendo nuestro useEffect como se puede ver en la siguiente imagen donde nos informa de que no estamos dependiendo en el array de dependencias la variable analyticData que estamos usando dentro del código del efecto:

Nota: en el caso de la imagen que se está mostrando en el proyecto en el que estamos trabajando tiene marcado este tipo de error de ESLint como un warning lo que implicará que no fallará el proceso de linter del código pero podría estar establecía a un nivel error y por lo tanto que bajo ningún concepto pase esta fase y por lo tanto no poder desplegar el código.

Una forma de solventar este problema en ESLint y siempre que estemos seguros de lo que estamos haciendo es informarle de que el código es correcto y por lo tanto que no debería preocuparse por ello con algo como lo siguiente:

useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

Con el comentario anterior lo que estamos haciendo es informar a ESLint que no deberá preocuparse de la línea a la que acompaña el comentario puesto que está correcta por lo que el warning o error desaparecerá y no podremos continuar con la construcción de nuestro software.

De hecho muchos desarrolladores no pueden decir que esta solución es correcta puesto que el código anterior es correcto y por lo tanto no hay ningún tipo de error dentro del mismo. Y eso es totalmente cierto.

¿Qué pasa cuando cambia analyticsData?

Hasta ahora todo lo que hemos mostrado es correcto pero tenemos que pararnos a pensar unos instantes en qué es lo que debería pasar en el caso de analysticsData. En el ejemplo que hemos desarrollado esto no puede llegar a suceder puesto que el valor que tiene asociado dicha variable está definido de forma estática dentro del propio componente y por lo tanto no cambiará.

Pero ¿qué sucederá en el caso de que se trate, por ejemplo, de una atributo del estado? Es decir, ¿qué sucederá en el caso de que ahora el código de nuestro componente esté definido de la siguiente manera?

import { useEffect, useState } from 'react'
import { trackEvent } from '@/utils'

export const Page = () => {
  const [analyticsData, setAnalysticsData] = useState({ userId: 1})

  useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  return <div></div>
}

Si el código de nuestro componete se queda tal y como está ahora ¿qué es lo que pasará si más adelante queremos usar la función setAnalyticsData en otra parte del código con el fin de cambiar el valor que tiene asociado analyticsData? Pues tenemos que pensar que este cambio probablemente debería lanzar una nueva ejecución de trackEvent pero nuestro código no es capaz de ello puesto que analyticsData no está recogido dentro del array de dependencias que está asociado al useEffect y por lo tanto se perdería el trackeo de los datos.

Así pues si dejamos este código como está como le hemos indicado a ESLint que no nos informe de los posibles errores derivados de no poner todas las variables que se están usando dentro del array de dependencias de useEffect sería más que posible que nos olvidásemos de ello y por lo tanto que el bug que hay en nuestro código viaje a producción provocando la pérdida de datos estadísticos.

Así pues la solución en estos casos pasa por quitar el comentario que hemos puesto para ESLint y proporcionar la variable de estado en el array de dependencias asociado a useEffect dejando algo como lo siguiente:

useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [analyticsData])

De esta manera pese a que se puedan realizar nuevos re-renders del componente mientras que el valor que el objeto que tiene asociado analysticsData no cambie no se ejecutará el código que está recogido dentro del useEffect con lo que conseguiremos que este código se ejecutará la primera vez que se renderizará el componete (en concreto cuando este es montado en el DOM) y luego siempre que cambie el objeto que tenga asociado analysticsData.

¿Qué solución hay para las variables del componente?

No olvidemos que si analyticData está recogido simplemente como una variable dentro del componente y lo escribimos dentro del array de dependencias de useEffect acabaremos con un bucle infinito de re-renderizados.

import { useEffect } from 'react'
import { trackEvent } from '@/utils'

export const Page = () => {
  const analyticsData = { userId: 1 }

  useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [])

  return <div></div>
}

En situaciones como la anterior lo primero en lo que tenemos que fijarnos en que la variable analyticsData la estamos usando únicamente dentro de useEffect por lo que un buen refactor será definirla como una variable dentro del código del propio efecto lo que provocará que se quite el error de ESLint:

import { useEffect } from 'react'
import { trackEvent } from '@/utils'

export const Page = () => {

  useEffect(() => {
    const analyticsData = { userId: 1 }
    trackEvent('pageEvent', analyticsData)
  }, [])

  return <div></div>
}

Como acabamos de ver esta es una solución elegante y muy sencilla de implementar siempre que la variable en cuestión vaya a ser utilizada únicamente dentro del useEffect.

No obstante, como podemos imaginar, una variable como analyticsData no va a ser utilizada únicamente en useEffect por lo que tenemos que encontrar otra solución y para ello nos vamos a tener que apoyar en el hook useMemo como sigue:


import { useEffect, useMemo } from 'react'
import { trackEvent } from '@/utils'

export const Page = () => {
  const analyticsData = useMemo(() => ({ userId: 1 }), [])

  useEffect(() => {
    trackEvent('pageEvent', analyticsData)
  }, [analyticsData])

  return <div></div>
}

Con esto lo que conseguimos es que React memorice el valor de analysticsData (o, dicho de otra manera, lo convertirá en una referencia establece entre distintos re-renders) mientras que no cambien las dependencias que están recogidas en el array de dependencias asociada con useMemo que, como vemos, estará vacía lo que supone que asignará el valor cuando se monte el componente y permancerá siendo el mismo con independencia del número de re-renders del componente en cuestión.

Nota: en el caso de estar trabajando con una función en vez de una variable como parte de las dependicas que tendrá useEffect deberemos usar useCallback y todo funcionará exactamente igual.

Ahora bien ¿estaremos seguros de que no nos vamos a olvidar de ninguno de estos pasos cuando estemos construyendo nuestros componentes de React? ¿no son demasiados? ¿se nos quedará alguno por el camino? En realidad son varios los aspectos en los que nos tenemos que fijar (arrays de dependencias, usos de hooks, etc.) que lo que acabarán provocando es que podamos olvidarnos de algunos de ellos y por lo tanto que nuestro código no esté correctamente construido.

Sin embargo este es uno de los patrones que tenemos que seguir si queremos construir nuestros componentes de forma correcta en React puesto que la alternativa sería pasar a trabajar con otros frameworks (como es el caso de Vue que no tiene este problema). Ahora bien sabemos que el React Team está trabajando en solucionar este problema por lo que se trata de algo pasajero y para ello está desarrollando el que se conoce como useEffectEvent o probablemente acabe desarrollando una versión de useEffect que acabe manejando de forma interna el array de dependencias por nosotros.

Hasta que esto ocurra deberemos aprender la técnica que acabamos de describir de tal manera que podamos asegurar que todas las variables y funciones que estemos usando dentro de useEffect estén recogidas dentro del array de dependencias que tiene asociado por lo que tendremos que evitar la solución basada en ignorar el error que nos muestra ESLint.