Domina el Hook useEffect en React

  Introducción a los Hooks

Los Hooks son funciones especiales que te permiten “engancharte” a características de React desde componentes funcionales. Fueron introducidos en React 16.8 para permitir usar estado y otras características de React sin escribir componentes de clase.

Hook de Efecto useEffect

El hook useEffect en React permite ejecutar efectos secundarios en componentes funcionales, como llamadas a APIs, suscripciones, manipulaciones del DOM, o cualquier operación que no se pueda hacer directamente durante el renderizado. Se ejecuta después de que el componente se renderiza.

Sintaxis Básica

jsx
import { useEffect } from 'react';

function ComponenteConEfecto() {
// Ejecución inicial del componente
useEffect(() => {
  // Código que quieres ejecutar cuando el componente se monta
  console.log('Componente montado');

  // Retorno opcional: función de limpieza que se ejecuta cuando el componente se desmonta
  return () => {
    console.log('Componente desmontado');
  };
}, [/* dependencias */]); // Dependencias vacías: solo se ejecuta una vez al montar y desmontar
  Explicación

El hook useEffect en React permite ejecutar efectos secundarios en componentes funcionales, como actualizaciones de datos o suscripciones a eventos. A continuación, te explicamos sus principales elementos:

  • Función de efecto: El primer argumento de useEffect es una función que se ejecuta después de cada renderizado del componente. Aquí puedes realizar acciones como llamadas a API, suscripciones o cambios en el DOM.

  • Función de limpieza: Al retornar una función dentro de useEffect, defines una acción de limpieza que se ejecutará antes de desmontar el componente o antes de que el efecto se vuelva a ejecutar. Esto es útil para liberar recursos, como detener suscripciones o limpiar temporizadores.

  • Array de dependencias: El segundo argumento es un array de dependencias que indica cuándo debe ejecutarse el efecto. Si el array está vacío ([]), el efecto se ejecutará solo una vez al montar el componente y no se volverá a ejecutar en renderizados futuros. Si incluye variables o estados, el efecto se ejecutará cada vez que alguno de ellos cambie.

Ejemplo Práctico

jsx
import React, { useState, useEffect } from 'react';

function EjemploUseEffect() {
const [contador, setContador] = useState(0);

useEffect(() => {
  document.title = `Has clickeado ${contador} veces`;
}, [contador]);  // Se ejecuta cada vez que el contador cambia

return (
  <div>
    <p>Has clickeado {contador} veces</p>
    <button onClick={() => setContador(contador + 1)}>Clic aquí</button>
  </div>
);
}
  Explicación

  • useEffect actualiza el título de la página cada vez que cambia el contador.
  • Al pasar [contador] como dependencia, el efecto se ejecuta solo cuando contador cambia. Si se pasa un array vacío ([]), solo se ejecutaría una vez, al montarse el componente.

useEffect con Dependencia Vacía

Ejecución Solo al Montar el Componente. Este ejemplo muestra cómo usar useEffect* con un array vacío ([]) para ejecutar un efecto solo una vez, cuando el componente se monta, ideal para inicializaciones o llamadas a APIs.

jsx
import React, { useEffect } from 'react';

function Componente() {
useEffect(() => {
  console.log("Este efecto se ejecuta solo una vez, al montarse el componente");

  // Aquí puedes poner código que solo se ejecute una vez, como una llamada a una API

}, []);  // El array vacío significa que solo se ejecuta una vez

return (
  <div>
    <p>Componente montado</p>
  </div>
);
}
  Explicación

  • El efecto dentro de useEffect se ejecuta solo una vez, justo después de que el componente se haya montado.
  • El array vacío [] como segundo argumento indica que no hay dependencias, por lo que el efecto solo se ejecuta una vez cuando el componente se monta por primera vez.

useEffect sin Array de Dependencias

Si no proporcionas un array de dependencias en useEffect, el efecto se ejecutará después de cada renderizado del componente, lo que incluye la primera vez que el componente se monta. Esto puede ser útil cuando deseas ejecutar un efecto cada vez que el componente se renderiza, sin depender de ningún valor específico.

jsx
import { useEffect, useState } from 'react';

function ComponenteSinDependencias() {
const [contador, setContador] = useState(0);

// Este useEffect se ejecuta después de cada renderizado
useEffect(() => {
  console.log('El componente ha sido renderizado o actualizado');

  // Función de limpieza (opcional) que se ejecutará antes de que el efecto se ejecute nuevamente o cuando el componente se desmonte
  return () => {
    console.log('Limpiando efecto');
  };
}); // Sin array de dependencias: se ejecuta en cada renderizado

const incrementarContador = () => {
  setContador(contador + 1);
};

return (
  <div>
    <p>Contador: {contador}</p>
    <button onClick={incrementarContador}>Incrementar</button>
  </div>
);
}

export default ComponenteSinDependencias;
  Explicación

  • Sin array de dependencias: Al no pasar un array de dependencias ([]), useEffect se ejecuta siempre que el componente se renderiza. Esto incluye tanto la primera ejecución al montar el componente como cualquier actualización posterior del componente.
  • Función de limpieza: Si la función de limpieza es definida (con return dentro de useEffect), se ejecutará antes de la siguiente ejecución del efecto o cuando el componente se desmonte.
  • Actualización del estado: Al hacer clic en el botón para incrementar el contador, se provoca un nuevo renderizado, lo que desencadenará la ejecución del useEffect de nuevo.

Este enfoque se utiliza cuando necesitas realizar alguna acción cada vez que el componente se renderiza, sin importar qué valores hayan cambiado.

Otro ejemplo con useEffect sin Array de Dependencias

En React, al usar useEffect sin dependencias, el efecto se ejecuta después de cada renderizado. Esto es útil para realizar acciones en cada actualización del componente, como manejar suscripciones o temporizadores. También puedes incluir una función de limpieza para ejecutar antes de la siguiente ejecución del efecto o cuando el componente se desmonte.

jsx
import React, { useEffect, useState } from 'react';

function ComponenteSinDependencias() {
const [contador, setContador] = useState(0);

// Este useEffect se ejecuta después de cada renderizado
useEffect(() => {
  console.log('El componente ha sido renderizado o actualizado');

  // Función de limpieza (opcional) que se ejecutará antes de que el efecto se ejecute nuevamente o cuando el componente se desmonte
  return () => {
    console.log('Limpiando efecto');
  };
}); // Sin array de dependencias: se ejecuta en cada renderizado

const incrementarContador = () => {
  setContador(contador + 1);
};

return (
  <div>
    <p>Contador: {contador}</p>
    <button onClick={incrementarContador}>Incrementar</button>
  </div>
);
}

export default ComponenteSinDependencias;

Consumir una API REST con useEffect

Dentro del useEffect puede definirse una función asíncrona (por ejemplo, para consultar una API) y llamarla inmediatamente al cargar el componente. Sin embargo, como useEffect no puede ser directamente asíncrono, se recomienda definir la función asíncrona dentro de él y luego invocarla.

jsx
import React, { useState, useEffect } from 'react';

function Componente() {
const [datos, setDatos] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  // Definir una función asíncrona dentro de useEffect
  const fetchDatos = async () => {
    try {
      const response = await fetch('https://api.example.com/datos');
      const data = await response.json();
      setDatos(data);
    } catch (error) {
      console.error('Error al obtener los datos:', error);
    } finally {
      setLoading(false); // Marcar como cargado una vez se termine
    }
  };

  fetchDatos(); // Llamar a la función asíncrona

}, []);  // El array vacío asegura que solo se ejecute una vez

return (
  <div>
    <h2>Datos de la API</h2>
    {loading ? (
      <p>Cargando...</p>
    ) : (
      <pre>{JSON.stringify(datos, null, 2)}</pre>
    )}
  </div>
);
}

export default Componente;
  Explicación

  • Función fetchDatos: Dentro de useEffect, se define una función asíncrona fetchDatos que consulta la API y maneja la respuesta.
  • Llamada a la API: Se utiliza fetch para obtener los datos de la API y luego se actualiza el estado con los datos obtenidos.
  • Manejo de Carga: Se mantiene un estado loading para mostrar un mensaje de “Cargando…” mientras los datos se están obteniendo, y después se muestra la respuesta.

useEffect con Dependencias

Cuando usas dependencias en useEffect, el efecto se ejecuta cada vez que las dependencias cambian. Si pasas variables dentro del array de dependencias, React ejecutará el efecto nuevamente siempre que esas variables cambien.

Aquí tienes un ejemplo donde se consulta una API cada vez que cambia el valor de una variable query (por ejemplo, una palabra clave de búsqueda)

jsx
import React, { useState, useEffect } from 'react';

function Componente() {
const [query, setQuery] = useState('react');
const [datos, setDatos] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  const fetchDatos = async () => {
    setLoading(true);
    try {
      const response = await fetch(`https://api.example.com/search?q=${query}`);
      const data = await response.json();
      setDatos(data);
    } catch (error) {
      console.error('Error al obtener los datos:', error);
    } finally {
      setLoading(false);
    }
  };

  fetchDatos(); // Llamar a la función asíncrona

}, [query]); // El efecto se ejecuta cada vez que cambia 'query'

return (
  <div>
    <h2>Buscar Datos</h2>
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)} // Actualizar la query
      placeholder="Buscar..."
    />
    {loading ? (
      <p>Cargando...</p>
    ) : (
      <pre>{JSON.stringify(datos, null, 2)}</pre>
    )}
  </div>
);
}

export default Componente;
  Explicación

  • Dependencia query: El useEffect ahora depende de la variable query. Cada vez que cambie el valor de query (como resultado de escribir en el input), se ejecutará nuevamente la función que consulta la API.
  • Consulta a la API: La URL de la API se construye dinámicamente en función de query, por lo que el efecto se ejecuta con la nueva consulta cada vez que el valor de query cambia.
  • Actualización del estado query: Cuando el usuario escribe en el campo de texto, se actualiza query, lo que dispara una nueva ejecución del efecto.

Múltiples useEffect

En React, puedes tener múltiples useEffect dentro de un mismo componente, cada uno con diferentes dependencias. Los useEffect se ejecutarán de forma independiente según sus propias dependencias.

jsx
import React, { useState, useEffect } from 'react';

function Componente() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('');

// Efecto 1: Imprimir el valor de "count" cuando cambia
useEffect(() => {
  console.log('El valor de count ha cambiado:', count);
}, [count]); // Se ejecuta cuando "count" cambia

// Efecto 2: Establecer un mensaje después de 2 segundos
useEffect(() => {
  const timer = setTimeout(() => {
    setMessage('¡Han pasado 2 segundos!');
  }, 2000);

  // Limpiar el timeout al desmontar el componente
  return () => clearTimeout(timer);
}, []); // Este efecto solo se ejecuta una vez cuando el componente se monta

return (
  <div>
    <h2>Contador: {count}</h2>
    <button onClick={() => setCount(count + 1)}>Aumentar contador</button>
    <p>{message}</p>
  </div>
);
}

export default Componente;
  Explicación

  • Primer efecto: Imprime en la consola el valor de count cada vez que cambia. Esto ocurre siempre que presionas el botón para aumentar el contador.
  • Segundo efecto: Muestra un mensaje después de 2 segundos de montar el componente. Este useEffect solo se ejecuta una vez cuando el componente se carga por primera vez.

Este ejemplo es más sencillo y muestra cómo usar múltiples efectos para realizar acciones diferentes, como observar cambios en el estado y ejecutar código en el montaje del componente.

Uso de Objetos con useEffect en React

En este ejemplo, useEffect depende de un objeto y se activa cada vez que alguna de sus propiedades cambia, mostrando cómo reaccionar a las actualizaciones de un objeto en el estado.

jsx
import { useEffect, useState } from 'react';

function ComponenteConObjeto() {
const [usuario, setUsuario] = useState({
  nombre: 'Urian Viera',
  edad: 35
});

// useEffect que depende del objeto "usuario"
useEffect(() => {
  console.log(`Nombre del usuario: ${usuario.nombre}`);
  console.log(`Edad del usuario: ${usuario.edad}`);

  // Efecto de limpieza opcional
  return () => {
    console.log('Limpiando efecto del usuario...');
  };
}, [usuario]); // Se ejecuta cada vez que cambia alguna propiedad del objeto "usuario"

// Cambia los datos del usuario al hacer clic
const actualizarUsuario = () => {
  setUsuario((prevUsuario) => ({
    ...prevUsuario,
    edad: prevUsuario.edad + 1,
  }));
};

return (
  <div>
    <p>Nombre: {usuario.nombre}</p>
    <p>Edad: {usuario.edad}</p>
    <button onClick={actualizarUsuario}>Aumentar Edad</button>
  </div>
);
}

export default ComponenteConObjeto;
  Explicación

  • Dependencia del objeto: Al pasar [usuario] como dependencia, el useEffect se ejecutará cada vez que haya un cambio en el objeto usuario, como cuando cambia nombre o edad.
  • Función de limpieza: Opcionalmente, si necesitas limpiar recursos o realizar una acción al desmontar el efecto, puedes incluir una función de limpieza dentro del useEffect.
  • Actualización del objeto: La función actualizarUsuario usa setUsuario para modificar solo la propiedad edad del objeto, conservando el resto del objeto usuario con el operador spread (...prevUsuario).

Uso de null o undefined con useEffect

  null o undefined como Dependencia en useEffect

Cuando una dependencia en useEffect es null o undefined, el efecto puede comportarse de las siguientes maneras:

  • Ejecución inicial y cambios: El useEffect aún se ejecuta al inicio y luego cada vez que el valor de la dependencia cambia, incluso si cambia a null o undefined.
  • Comparación de dependencias: React compara el valor actual de la dependencia con su valor anterior para decidir si ejecuta el efecto nuevamente. Cuando una dependencia cambia de undefined a null (o viceversa), React considera esto como un cambio, por lo que el efecto se ejecutará nuevamente.

Aquí tienes un ejemplo sencillo para ver cómo null o undefined como dependencia puede activar el useEffect:

jsx
import { useEffect, useState } from 'react';

function ComponenteConDependenciaNula() {
const [data, setData] = useState(null);

// Este useEffect depende de "data", y se ejecutará cada vez que "data" cambie
useEffect(() => {
  if (data === null) {
    console.log('Data está en null');
  } else if (data === undefined) {
    console.log('Data está en undefined');
  } else {
    console.log(`Data tiene valor: ${data}`);
  }
}, [data]); // El efecto se ejecuta cada vez que "data" cambia, incluso si cambia a null o undefined

// Simula cambios de estado
const establecerData = (valor) => setData(valor);

return (
  <div>
    <p>{data !== null && data !== undefined ? `Data: ${data}` : 'Data es null o undefined'}</p>
    <button onClick={() => establecerData('Nuevo valor')}>Establecer Data</button>
    <button onClick={() => establecerData(null)}>Establecer null</button>
    <button onClick={() => establecerData(undefined)}>Establecer undefined</button>
  </div>
);
}

export default ComponenteConDependenciaNula;
  Explicación del comportamiento

  • Cambio a null: Al hacer clic en el botón “Establecer null”, data cambia a null, lo que activa el useEffect y muestra Data está en null.
  • Cambio a undefined: Al hacer clic en el botón “Establecer undefined”, data cambia a undefined, activando nuevamente el useEffect y mostrando Data está en undefined.
  • Cambio a un valor distinto: Cualquier cambio de data activa el useEffect, ya sea a null, undefined o cualquier otro valor, siempre que sea diferente al valor anterior.

Este manejo es útil para condiciones en las que necesitas saber si un valor está null o undefined para tomar acciones específicas dentro del useEffect.

Malas y buenas prácticas usando el hook useEffect en React


1. Efectos que se ejecutan innecesariamente

jsx
// ❌ Mala práctica:
// Ejecutar el efecto en cada renderizado sin tener un array de dependencias adecuado.

useEffect(() => {
console.log("Este efecto se ejecuta después de cada renderizado");
});

// ✅ Buena práctica: 
// Definir correctamente las dependencias para evitar ejecuciones innecesarias.

useEffect(() => {
console.log("Este efecto se ejecuta solo cuando `variable` cambia");
}, [variable]); // Solo se ejecuta cuando `variable` cambia

2. Olvidar incluir dependencias necesarias

jsx
// ❌ Mala práctica:
// Olvidar agregar dependencias a un array vacío o no incluir todas las dependencias necesarias.

useEffect(() => {
console.log("Efecto con dependencias incompletas");
// Se usa `count` pero no está en las dependencias
}, []);


// ✅ Buena práctica: 
// Asegúrate de incluir todas las variables usadas dentro del efecto en el array de dependencias.

useEffect(() => {
console.log("Efecto con dependencias completas");
}, [count]); // Se incluye `count` para que el efecto se ejecute cuando cambie

3. Efectos que modifican el estado de manera descontrolada

jsx
// ❌ Mala práctica:
// Modificar el estado dentro de un efecto sin control, lo que puede causar ciclos infinitos de renderizados.

useEffect(() => {
setCount(count + 1);  // Esto provoca un ciclo infinito
}, [count]);


// ✅ Buena práctica: 
// Usar funciones de actualización de estado que eviten ciclos infinitos, usando el valor más reciente del estado.

useEffect(() => {
setCount(prevCount => prevCount + 1);  // Evita ciclos infinitos
}, [count]);

4. No limpiar recursos o suscripciones

jsx
// ❌ Mala práctica:
// No limpiar suscripciones, temporizadores u otros recursos al desmontar el componente.

useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
// No se limpia el intervalo
}, []);


// ✅ Buena práctica: 
// Limpiar los recursos en la función de limpieza para evitar fugas de memoria.

useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer);  // Limpiar el intervalo
}, []);

5. Efectos dependientes de objetos o arrays

jsx
// ❌ Mala práctica:
// No manejar adecuadamente dependencias de objetos o arrays en useEffect, lo que puede provocar ejecuciones innecesarias.

const settings = { theme: 'dark' };
useEffect(() => {
console.log(settings.theme);  // Se ejecutará en cada renderizado
}, [settings]);  // Los objetos/arrays son referenciados por su identidad


// ✅ Buena práctica: 
// Usar variables primitivas o técnicas de memoización (como useMemo o useCallback) para evitar cambios de referencia innecesarios.

const settings = useMemo(() => ({ theme: 'dark' }), []);
useEffect(() => {
console.log(settings.theme);  // Se ejecuta solo cuando `settings` cambia
}, [settings]);

6. Dependencias vacías innecesarias

jsx
// ❌ Mala práctica:
// Usar un array vacío sin entender bien su funcionamiento, lo que puede llevar a efectos que se ejecutan solo una vez pero con dependencias ocultas.

useEffect(() => {
// Ejecutar una vez al montar el componente
}, []);  // Peligroso si el efecto depende de alguna variable interna


// ✅ Buena práctica: 
// Siempre revisar que el efecto no dependa de ningún valor que deba estar en las dependencias.

useEffect(() => {
// Ejecutar código al montar, pero asegúrate de que no dependa de ningún valor mutable
}, []);  // Solo si el efecto no depende de ningún valor mutable

7. Uso innecesario de efectos en lugar de optimizar lógica

jsx
// ❌ Mala práctica:
// Utilizar useEffect para ejecutar lógica que podría hacerse de manera más sencilla sin un efecto.

useEffect(() => {
if (count > 10) {
  console.log('Contador mayor a 10');
}
}, [count]);  // Usar useEffect aquí es innecesario


// ✅ Buena práctica: 
// Simplemente usar la lógica directamente en el cuerpo del componente.

if (count > 10) {
console.log('Contador mayor a 10');
}

8. Uso de async/await directamente dentro de useEffect

jsx
// ❌ Mala práctica:
// Usar async/await directamente dentro de useEffect, lo que puede causar advertencias o un comportamiento inesperado.

useEffect(async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
}, []);


// ✅ Buena práctica: 
// Usar una función asíncrona dentro del useEffect de manera separada, para manejar correctamente las operaciones asincrónicas.

useEffect(() => {
const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
};

fetchData();
}, []);
  Resumen Importante

  • Usa el array de dependencias correctamente para evitar ejecuciones innecesarias.
  • Siempre limpia los recursos en la función de retorno si el efecto crea suscripciones o temporizadores.
  • Evita modificar el estado de manera descontrolada dentro del useEffect para prevenir ciclos infinitos.
  • Maneja correctamente los objetos/arrays como dependencias, usando técnicas de memoización si es necesario.
  • Revisa que el efecto solo dependa de los valores necesarios y no agregues dependencias innecesarias.
  • No uses async/await directamente en useEffect. En su lugar, define funciones asíncronas dentro del efecto para evitar errores.