useEffect es el segundo React Hook mas utilizado y nos permite llevar a cabo efectos secundarios en componentes funcionales de React. Peticiones de datos, establecimiento de suscripciones y actualizaciones manuales del DOM en componentes de React serían ejemplos de efectos secundarios.
En este articulo vamos a aprender a utilizar el Hook useEffect, para que sirve, los tipos de efectos secundarios que puede manejar y daremos un par de recomendaciones para usarlo correctamente y evitar los errores mas comunes.
Que es el Hook useEffect?
El Hook useEffect nos permite llevar a cabo efectos secundarios en componentes funcionales. Este Hook que recibe una función para ejecutarse justo después de que se haya mostrado el renderizado en pantalla, aunque también tiene la opción de activarse cuando cambian ciertos valores. Es muy importante destacar que la función fundamental de el hook useEffect es ejecutar código cada vez que nuestro componente se renderiza.
La forma de declarar un hook de efecto es:
useEffect(()=>{},[ ]);
Donde:
- ( ) =>{ }:Es una función que se ejecuta luego de que se renderiza el componente.
- [ ]:Listado de props o variables de estado que el hook estará inspeccionando para decidir si se ejecuta o no, solo se ejecuta cuando el valor de alguno de los elementos del array cambia, para ello React lo que va a hacer es comparar el valor del renderizado anterior con el siguiente. Si el array esta vacío React interpreta que el efecto no depende de ningún valor de las props o el estado.
Nota:Un ejemplo de un componente funcional que usa el Hook useEffect es el siguiente:
Para acceder al articulo donde vimos el hook useState puede ir a este artículo: Conociendo en profundidad el React Hook useState.
import React, { useState, useEffect } from 'react'; // Importamos el Hook useState y useEffect desde React function Contador() { const [count, setCount] = useState(0); // Actualiza el título del sitio, después del renderizado. // Similar a componentDidMount y componentDidUpdate. useEffect(() => { // Actualiza el título del documento usando la API del navegador document.title = `Pulsaste el botón ${count} veces`; }),[count]; return ( <div> <h1>El botón ha sido pulsado {count} veces</h1> <button onClick={() => setCount(count + 1)}> Púlsame </button> </div> ); };
En este ejemplo sencillo ocurre lo siguiente:
- Nada más entrar en la página el título de la página se actualiza ya que se ejecuta useEffect al montarse nuestro componente.
- Cada vez que hacemos click en el componente. Cuando el state cambia, esto dispara un nuevo renderizado y, al renderizarse de nuevo, se vuelve a ejecutar la función que le hemos pasado a useEffect.
Un ejemplo un poco mas complejo y real es el que vamos a ver a continuación en el cual vamos a usar el useEffect para traer datos desde una API:
import React, { useState, useEffect } from 'react'; function Pokemones () { // list es la variable de estado que va a guardar el listado de los pokemon que devuelve la api const [list, setList] = useState(); useEffect(() => { // funciones a ejecutar luego de que se renderiza el componente async function getData(url) { //función que devuelve la lista de pokemones const response = await fetch(url); const data = await response.json(); setList(data.results); } //pasamos a getData la url de consulta de la pokeapi getData("https://pokeapi.co/api/v2/pokemon?limit=151"); }); return ( <div> <h2>Listado de Pokemones Originales:</h2> <ul>{list && list.map((item) => <li>{item.name} </li>)}</ul> </div> ); };En este ejemplo el Hook va a hacer un llamado a la API justo después de renderizar el componente para posteriormente dibujar los datos. Pero hay un pequeño problema y es que si se ejecuta el código, se va a estar haciendo la petición constantemente, convirtiendo la app en un loop infinito.
Esto sucede porque a pesar de que el Hook useEffect sabe que hacer, no tiene alguna referencia o dato que le diga en que momento hacerlo, es decir, no tiene una forma de saber cuando parar o continuar con el proceso. Para evitarlo, hay que hacer uso del segundo parámetro, que define cuándo se actualiza el componente.
El código del useEffect para decirle explícitamente cuando ejecutar la función quedaría de la siguiente manera:
useEffect(() => { async function getData(url) { const response = await fetch(url); const data = await response.json(); setList(data.results); } getData("https://pokeapi.co/api/v2/pokemon?limit=151"); },[]); //usamos el segundo argumento [] para useEffect solo se ejecute la primera vezPueden ver el ejemplo funcionando en este link de codepen: Ejemplo de useEffect con consulta a API
¿Para que sirve useEffect?
Con useEffect podemos emular algunos métodos de ciclo de vida dentro de componentes funcionales y aplicar la lógica que requiera nuestro proyecto, por ejemplo, podemos emular:
- componentDidMount, se ejecuta cuando el componente es montado.
- componentDidUpdate, se ejecuta cuando el componente sufre un cambio en su state o props.
- componentWillUnmount, se ejecuta cuando el componente es desmontado.
- componentDidMount y componentDidUpdate.
- componentWillUnmount
En el siguiente código se ilustra como emular los métodos de ciclo de vida utilizando Hooks en un componente funcional:
useEffect(()=>{ //En el cuerpo de la función se emulan componentDidMount y componentDidUpdate console.log("componentDidMount"); console.log("componentDidUpdate"); return( //El return de useEffect es el equivalente a componentWillUnmount console.log("componentWillUnmount"); ) });
Tipos de efectos secundarios
Peticiones de datos, establecimiento de suscripciones y actualizaciones manuales del DOM en componentes de React serían ejemplos de efectos secundarios.
Hay dos tipos de efectos secundarios en los componentes de React: aquellos que no necesitan una operación de saneamiento y los que sí la necesitan:
-
Efectos sin saneamiento
En ciertas ocasiones, queremos ejecutar código adicional después de que React haya actualizado el DOM. Peticiones de red, mutaciones manuales del DOM y registros, son ejemplos comunes de efectos que no requieren una acción de saneamiento. Decimos esto porque podemos ejecutarlos y olvidarnos de ellos inmediatamente.
Desde el punto de vista del ciclo de vida esto se refiere a que nuestro componente solo ejecuta, componentDidMount y componentDidUpdate, y no requiere que hagamos nada al ser desmontado, ya que, no deja nada cargado en memoria, por lo tanto no necesitamos invocar el método componentWillUnmount.
Un ejemplo de un componente que utiliza un efecto sin saneamiento es el siguiente en donde vamos a utilizar navigator.geolocation el cual es una función de JavaScript que nos permite obtener la posición de un usuario a través del navegador. Donde navigator.geolocation.getCurrentPosition regresa una función con la posición del navegador de usuario.import React, { useState, useEffect } from 'react'; function Ubicacion () { const [latitud, setLatitud] = useState(0); const [longitud, setLongitud] = useState(0); //Ejemplo donde no requiere saneamiento useEffect(()=>{ //Emular los métodos de ciclo de vida //componentDidMount //componentDidUpdate console.log("componentDidMount"); console.log("componentDidUpdate"); //Posición if(navigator.geolocation){ navigator.geolocation.getCurrentPosition((pos)=>{ setLatitud(pos.coords.latitude.toFixed(0)); setLongitud(pos.coords.longitude.toFixed(0)); }); }else{ console.log("El navegador no soporta la geolocalización"); } },[latitud,longitud]); return (
Pueden ver el ejemplo funcionando en este link de codepen:Ejemplo de Efecto Sin Saneamiento{`Lat: ${latitud}, Lon: ${longitud}`}
-
Efectos con saneamiento
Este concepto hace referencia a que nuestro componente requiere llevar a cabo una limpieza cuando el componente es desmontado (componentWillUnmount), esta situación se puede dar por muchas razones:- Hemos realizado una suscripción con alguna API.
- Tenemos corriendo procesos como intervalos que se ejecutan cada determinado tiempo.
- Etc.
Import React, { useState, useEffect } from 'react'; const Contador = () => { const [contador, setContador] = useState(0); //Ejemplo donde si requiere saneamiento useEffect(()=>{ //Contador const intervalo = setInterval(() => { console.log("Intervalo...") setContador(contador + 1); }, 2000); return ()=>{ //componentWillUnmount console.log("componentWillUnmount"); clearInterval(intervalo); } },[contador]) return (
En el código anterior vemos claramente el saneamiento, ya que, en el cuerpo de la función inicializamos el intervalo y en el return del useEffect lo eliminamos, con esto logramos que no se queden intervalos ejecutándose innecesariamente.{contador}
Pueden ver el ejemplo funcionando en este link de codepen: Ejemplo de efecto con Saneamiento
Recomendaciones al utilizar useEffect
-
Usa varios efectos para separar conceptos
Uno de los problemas que sirvieron como motivación para crear los Hooks es que los métodos del ciclo de vida de las clases suelen contener lógica que no está relacionada, pero la que lo está, se fragmenta en varios métodos.
Para resolver esta situación React nos permite usar más de un Hook useEffect para separar lógica que no esta relacionada, pero que se deben de ejecutar ya sea en al montaje, en la actualización o en el desmontaje del componente.
Un ejemplo es el siguiente, en el cual tenemos un componente que combina la lógica del contador y el indicador de estado del amigo, esta logica esta separada en dos useEffect como forma de separar la lógica que no esta relacionada:function FriendStatusWithCounter(props) { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); // ... }
Los Hooks nos permiten separar el código en función de lo que hace en vez de en función del nombre de un método de ciclo de vida. React aplicará cada efecto del componente en el orden en el que han sido especificados. -
Mejora el rendimiento omitiendo efectos
En algunos casos, sanear o aplicar el efecto después de cada renderizado puede crear problemas de rendimiento, supongamos que necesitas hacer algo cuando tus variables de estado cambien, pues para ello, lo que puede hacer es utilizar un Hook de efecto que escuche a una variable de estado en particular o propiedad.
Así cada vez que haya una actualización, cada uno reaccionará a la variable de estado que esta escuchando, si esta no ha sufrido ningún cambio el proceso no se ejecutará y no necesitará recursos del navegador.
Si un Hook de efecto no se ejecuta, optimizarás los recursos y tu aplicación correrá mejor a la vista de tus usuarios.
Puedes indicarle a React que omita aplicar un efecto si ciertos valores no han cambiado entre renderizados. Para hacerlo, debemos pasar un array como segundo argumento opcional a useEffect.
Retomemos nuestro ejemplo y veamos el componente que contiene a nuestros dos Hooks que hemos venido desarrollando en los ejemplos de efectos con y sin saneamiento:const Hijo = (props)=>{ const [contador, setContador] = useState(0); const [latitud, setLatitud] = useState(0); const [longitud, setLongitud] = useState(0); //Consejo 1 - Separar procesos //Consejo 2 - Mejorar rendimiento separando efectos //Ejemplo donde no requiere saneamiento useEffect(()=>{ //Emular los métodos de ciclo de vida //componentDidMount //componentDidUpdate console.log("componentDidMount"); console.log("componentDidUpdate"); //Posición if(navigator.geolocation){ navigator.geolocation.getCurrentPosition((pos)=>{ setLatitud(pos.coords.latitude.toFixed(0)); setLongitud(pos.coords.longitude.toFixed(0)); }); }else{ console.log("El navegador no soporta la geolocalización"); } },[latitud,longitud]); //Ejemplo donde si requiere saneamiento useEffect(()=>{ //Contador const intervalo = setInterval(() => { console.log("Intervalo...") setContador(contador + 1); }, 1000); return ()=>{ //componentWillUnmount console.log("componentWillUnmount"); clearInterval(intervalo); } },[contador]) return(
{contador}
{`Lat: ${latitud}, Lon: ${longitud}`}
En el código anterior hemos optimizado nuestro código ya que hemos hecho que cada Hook sea independiente a la ejecución del otro y solo se lanzará cuando realmente sea necesario, para lograr esto lo que hicimos fue utilizar el segundo argumento que recibe useEffect, el cual es una arreglo que recibe todas las variables de estado o propiedades que queremos escuchar para ejecutoa el Hook.//Ejemplo donde no requiere saneamiento useEffect(()=>{ ..... }, [latitud, longitud] ) //Ejemplo donde si requiere saneamiento useEffect(()=>{ ....... },[contador])
Nota:
Si optimizas tu aplicación de esta manera, no olvides agregar dentro de los [] todos los valores que influyen en el ámbito de tu función de efecto, tanto variables de estado como las props, ya que de lo contrario estarías referenciando hacia datos obsoletos