Don’t lie to React

por | Nov 24, 2021 | Analítica de datos | 0 Comentarios

Desde la llegada de la versión 16.8, muchos desarrolladores React, que crecimos con las clases y su estructurado ciclo de vida, hemos tenido que adaptarnos a una nueva realidad: los hooks.

Al principio, todo parecía cuestión de acostumbrarse: nuestro setState() se podría sustituir fácilmente por un conciso useState(), por ejemplo. Sin embargo, cuando la lógica de los componentes se hacía más compleja y el empleo de useEffect() – el hook de efecto – se volvía más que imprescindible, aparecieron nuevos problemas en nuestro horizonte.

Y es que, al entrar en escena useEffect(), nuestros minimalistas componentes hooks se volvieron inmanejables, incontrolables, salvajes… Decenas de mensajes de consola nos avisaban de dependencias que debían ser declaradas y de arrays que debían añadirse. El fin de la historia, y el principio de nuestra tranquilidad, pasó a estar en manos de un salvador comentario de ESLint (eslint-disable-next-line react-hooks/exhaustive-deps) que nos libraba del caos y de todo mal.

Sin embargo, si queremos llegar a ser buenos desarrolladores debemos partir del hecho de que, deshabilitar reglas y validaciones, no nos llevaran por el buen camino.

En este artículo, explicaremos, además de cómo utilizar correctamente useEffect() y el array de dependencias, por qué es una mala, malísima idea mentir a React.

¿Qué es useEffect()?

El hook de efecto de React, useEffect(), como sabemos, permite llevar a cabo efectos secundarios en componentes funcionales. Se dispara, para ello, después de cada renderizado, pudiendo existir uno o más dentro de un mismo componente. Para los de la vieja guardia, diremos que puede funcionar como componentDidMount(), componentDidUpdate() e, incluso, como componentWillUnmount(). Es sin duda, una herramienta realmente potente.

En el siguiente ejemplo, useEffect() funciona como componentDidUpdate():

const myComponent = () => {
   const [number, useNumber] = useState(0);

   useEffect(() => {
      setNumber((prev) => prev + 1);
   })

   return <p>{number}</p>
}

El valor de la variable de estado number se actualizará cada vez que se renderice el componente, que, en el caso que nos ocupa, provocará un loop infinito. Y es que el cambio de valor de number mediate setNumber() propiciará un re-renderizado del componente, que provocará un nuevo cambio de valor, que, a su vez, implicará un nuevo renderizado.

Desconozco si alguien se ha topado alguna vez con un caso como este, pero es algo que React permite llevar a cabo. No obstante, por consola, se nos advertirá de lo siguiente:

React Hook useEffect contains a call to ‘setNumber’. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [] as a second argument to the useEffect Hook.

(react-hooks/exhaustive-deps)

Nos avisa de que nuestro efecto contiene una llamada a la función setNumber(), y que no hemos declarado ningún array dependencias. Esto puede causar (y de hecho lo causa) un loop infinito de actualizaciones. También, se nos anima a pasar un array vacío ([]) como segundo argumento de nuestro useEffect.

Haciendo caso de las recomendaciones, nuestro código quedaría de la siguiente manera:

const myComponent = () => {
   const [number, useNumber] = useState(0);

   useEffect(() => {
      setNumber((prev) => prev + 1);
   }, [])

   return <p>{number}</p>
}

Sin embargo, vemos que el comportamiento de nuestro useEffect() cambia totalmente. El efecto pasa a dispararse solo una vez, aunque, eso sí, en la consola no aparece ya ningún warning.

¿Todo resuelto, entonces? ¿Qué ocurriría si nuestro caso de uso fuera, efectivamente, que la variable number se modificara cada vez que se renderiza el componente?

Una leyenda urbana sobre useEffect(): El array de dependencias vacío.

Algo que se nos graba a fuego cuando empezamos a trabajar con hooks es que un useEffect() solo se disparará una vez si añadimos un array vacío como segundo parámetro. Sin embargo, no es algo imprescindible para que esto se produzca y, además, como veremos más adelante, no es considerado, para nada, una buena práctica.

Veamos otro ejemplo

const myComponent = ({pets, getPets}) => {
   useEffect(() => {
     getPets();
   }, []);

   return (
      <ul>
          {pets.map(({name}) => <li>{name}</li>)}
      </ul>
   )
}

Nuestro componente está suscrito a un contexto (tipo Redux) que nos proporciona información sobre mascotas. Nos interesa que, al inicio del ciclo de vida del componente, realice una petición para obtener la información acerca de estas. Para ello, añadimos un useEffect(), con la llamada a la función getPets().

Si tuviéramos en cuenta «la ley del array vacío», nuestra primera aproximación podría ser añadir uno como segundo parámetro. Sí, nuestro useEffect() se comportaría como un excelente componentDidMount(), sin embargo, también obtendríamos un bonito warning donde se nos indicaría que debemos declarar, en el array de dependencias, nuestra función getPets().

React Hook useEffect has missing dependencies:  ‘getPets’. Either include them or remove the dependency array.

(react-hooks/exhaustive-deps)

No worries! Solo tenemos que hacer caso, de nuevo, al consejo de React.

const myComponent = ({pets, getPets}) => {
   useEffect(() => {
     getPets();
   }, [getPets]);

   return (
      <ul>
          {pets.map(({name}) => <li>{name}</li>)}
      </ul>
   )
}

Al añadir getPets() como dependencia, veremos como nuestro useEffect(), aún sin estar el array de dependencias vacío, se ejecutará solo una vez. ¡Maravilloso!

El array de dependencias

Es posible que el array de dependencias sea el gran desconocido de toda esta gran ecuación. A pesar de esto, los buenos desarrolladores React (y los que pretendemos serlo), deberíamos tenerlo como nuestro mejor aliado y amigo.

Como ya sabemos, y hemos visto en los anteriores ejemplos, el array de dependencias puede ser incluido como segundo término en nuestros useEffects(). Además, por increíble que pueda parecernos, es opcional.

Si no se incluye, el efecto se ejecutará cada vez que se renderice el componente. Por el contrario, si optamos por añadirlo (o las buenas prácticas nos obligan a ello), el efecto tendrá un comportamiento condicional: se disparará en el caso de que alguno de los miembros del array de dependencias cambie. Tras el renderizado, se compara el valor de estos antes del renderizado con el valor tras este. Si alguno cambia (sí, solo es cuestión de que uno de ellos cambie) el efecto se dispara. Increíble, ¿verdad?

¿Qué ocurre si se el array se encuentra vacío? Al no tener miembros que comparar, el efecto se lanzará solo una vez: una bonita explicación a nuestra pequeña leyenda urbana de más arriba.

Y es que, aunque parezca que está ahí para complicarnos la existencia, el array de dependencias se utiliza para mejorar la performance y reducir renderizados. Es especialmente mágico cuando trabajamos con valores primitivos, como cadenas, números o booleanos. Cuando los miembros de este array son objetos más o menos complejos, sigue siendo igual de mágico, solo que pasa a compararse por igualdad referencial, por lo que a veces tendremos que echar mano de otros hooks como useRef() (o a su aparición estelar en usePrevious()).

¿Y en serio hay que declarar todas las dependencias?

Siempre que usemos array de dependencias es importante declarar todas las variables y funciones involucradas. De no ser así, además de un bonito warning en la consola que nos lo recomendará, podremos conseguir side-effects no deseados.

En ocasiones, nos surge la necesidad de implementar un efecto cuando exista un cambio en cierta variable, pero no necesariamente cuando exista en otra. Tener lógicas complejas en nuestros useEffects(), que implican la intervención de muchas variables y/o funciones, con arrays de dependencias enormes, puede traducirse en efectos secundarios no controlados (ni deseados) que nos compliquen el desarrollo.

Sin embargo, mentir a React, es decir, no declarar las dependencias correctamente, puede provocar el caos en nuestras lógicas. Además es algo que, os puedo asegurar, siempre, siempre da la cara a futuro.

Es crucial tener claro que, si no declaramos ciertas variables/funciones, nuestro useEffect() nunca sabrá que estas pueden cambiar. Esto causará que no se modifiquen sus valores dentro del efecto, lo que puede conllevar un gran problema de desactualización.

const myComponent = ({total}) => {
   const [number, useNumber] = useState(0);

   useEffect(() => {
      setNumber((prev) => prev + 1);
   }, [])

   useEffect(() => {
      // El valor de number siempre será 0
      setNumber(total + number)
   }, [total])

   return <p>{number}</p>
}

Si retomamos el ejemplo de nuestro primer ejemplo, y añadimos un nuevo useEffect que se dispare cuando total (una prop externa) se actualice, comprobaremos que, si no añadimos number en nuestro array de dependencias, el valor de esta variable siempre será 0.

¿Y qué ocurre con las funciones? ¿También hay que declararlas?

La documentación de React nos aconseja que deben añadirse funciones que utilicen props o states. Y es que, como con las variables, si las funciones no se declaran debidamente, sus dependencias pueden llegar a estar desactualizadas.

En el primer useEffect() del código anterior, por ejemplo, no es necesario declarar setNumber() en el array de dependencias, ya que esta función no utiliza ni states ni props en su interior. De hecho, utilizar el callback interno de los hooks de efecto es una magnífica solución para ahorrarte problemas de dependencias. Como podemos ver más abajo, al utilizar el callback interno en el segundo useEffect() de nuestro ejemplo, podemos ahorrarnos incluir number dentro del array de dependencias.

const myComponent = ({total}) => {
   const [number, useNumber] = useState(0);

   useEffect(() => {
     setNumber((prev) => prev + 1);
   }, [])

   useEffect(() => {
     // El valor de number siempre será 0
     setNumber((prev) => total + prev)
   }, [total])

   return <p>{number}</p>
}

Para otro tipo de funciones, React recomienda incluir, si es posible, la función dentro del mismo useEffect() (declarando, eso sí, sus dependencias), sacar la función fuera del componente o utilizar useCallback(), con su respectivo array.

Por otra parte, debemos aclarar que el array de dependencias no es algo exclusivo de useEffect(); podemos encontrarlo en otros hooks como useMemo() (memoriza el resultado de un cálculo entre las llamadas de una función y entre los renders) y useCallback() (memoriza una devolución de llamada en sí, mediante igualdad referencial, entre renders)

Y en conclusión… Don’t lie to React

No mientas a React. Ya hemos explicado las consecuencias nefastas, en tanto a desactualización, que nos puede acarrear hacerlo. Además, la documentación al respecto no solo indica que no lo hagas, también que sustentes tus desarrollos en el uso de la exhaustive-deps rule, que es parte del eslint-plugin-react-hooks package.

Si tienes la necesidad de faltar a la verdad, probablemente, sea una consecuencia de que algo no vaya demasiado bien en tu código, y que este sea bastante mejorable. Si, aún así, quieres seguir adelante, hazlo con conocimiento de causa, pero luego no digas que no te hemos avisado. Don’t lie to React, my friend!

0 comentarios

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Conoce conceptos clave de la era digital en nuestro HUB

 

 

Descarga nuestro ebook con todo lo que necesitas saber sobre IoT Industrial.

Suscríbete a nuestra newsletter

Recibe las últimas noticias sobre innovación y nuevas tecnologías y mantente al día de los avances más destacados

Cesión de datos

Calle Factores 2, 41015 Seville

Phone: +34 618 72 13 58

Email: info@secmotic.com

MENU