📋 Tabla de contenidos
-
useState
– El fundamento del estado -
useEffect
– Efectos secundarios y ciclo de vida -
useContext
– Compartir estado sin props -
useReducer
– Lógica de Estado Compleja y predecible -
useRef
– Referencias, Almacenamiento y Escape -
useMemo
yuseCallback
– Optimizando el Rendimiento - Custom Hooks – Creando Lógica Reutilizable
- Errores Comunes y Soluciones
¿Qué es y para qué sirve?
useEffect
conecta tus componentes con sistemas externos. Permite ejecutar "efectos secundarios" después de que el componente se haya renderizado. Un efecto secundario es cualquier código que afecta a algo fuera del propio componente, como:
- Llamadas a una API para buscar datos.
- Suscripciones a eventos (ej.
window.addEventListener
). - Manipulación manual del DOM.
- Timers como
setInterval
osetTimeout
.
useEffect
reemplaza a los métodos de ciclo de vida componentDidMount
, componentDidUpdate
y componentWillUnmount
de los componentes de clase.
Sintaxis y el array de dependencias
useEffect(() => {
// ...tu código del efecto...
return () => {
// ...función de limpieza (opcional)...
};
}, [dependencias]);
La parte más importante de useEffect
es el array de dependencias. Controla cuándo se ejecuta el efecto:
-
[dependencia1, dependencia2]
: El efecto se ejecuta solo si alguna de las dependencias ha cambiado desde el último render. Este es el caso más común. -
[]
(array vacío): El efecto se ejecuta una sola vez, justo después del renderizado inicial (equivalente acomponentDidMount
). Perfecto para inicializaciones o fetching de datos que no cambian. - No se proporciona el array: El efecto se ejecuta después de cada renderizado. ¡Cuidado! Esto puede causar bucles infinitos si el efecto actualiza el estado.
La función de limpieza (return
) es opcional y se ejecuta:
- Antes de que el efecto se vuelva a ejecutar (si una dependencia cambió).
- Cuando el componente se desmonta (se elimina de la pantalla).
Es crucial para limpiar suscripciones, timers o listeners y evitar fugas de memoria.
Ejemplo práctico detallado: fetching de datos con estado de carga y error
Este es el caso de uso más común para useEffect
. Vamos a buscar datos de un Pokémon y a manejar los estados intermedios.
import React, { useState, useEffect } from 'react';
function PokemonInfo({ pokemonName }) {
const [pokemon, setPokemon] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!pokemonName) return;
// Reseteamos los estados al empezar una nueva búsqueda
setLoading(true);
setError(null);
setPokemon(null);
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`)
.then(response => {
if (!response.ok) {
throw new Error('No se encontró el Pokémon');
}
return response.json();
})
.then(data => {
setPokemon(data);
})
.catch(error => {
setError(error.message);
})
.finally(() => {
setLoading(false);
});
}, [pokemonName]); // El efecto depende del nombre del Pokémon
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
{pokemon && (
<>
<h3>{pokemon.name}</h3>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
</>
)}
</div>
);
}
🚀 Ver el código interactivo en CodeSandbox Haz clic para abrir el sandbox y experimentar con el código
✅ Buenas practicas y patrones comunes
- Un efecto, una responsabilidad: Si tienes lógicas no relacionadas, sepáralas en varios
useEffect
. Uno para fetching, otro para unevent listener
, etc. Esto hace el código más limpio. - Limpia siempre tus efectos: Si te suscribes a algo, crea un timer o un listener, siempre devuelve una función de limpieza para cancelarlo.
- No olvides las dependencias: El linter de React te suele avisar si usas una variable o función dentro del efecto pero no la incluyes en el array. ¡No lo ignores! Omitir dependencias puede causar bugs muy difíciles de rastrear.
- Funciones en el array de dependencias: Si una función se usa dentro de
useEffect
, debe estar en el array. Pero si esa función se redefine en cada render, causará que el efecto se ejecute siempre. La solución es definir la función dentro deluseEffect
o memorizarla conuseCallback
.
🚨 Errores comunes y cómo evitarlos
-
Error: El bucle infinito.
// MAL ❌ const [count, setCount] = useState(0); useEffect(() => { // Este efecto cambia el estado, lo que causa un re-render, // lo que vuelve a ejecutar el efecto, y así hasta el infinito. setCount(count + 1); }); // Sin array de dependencias
- Solución: Añade un array de dependencias. Si quieres que se ejecute solo una vez, que sea
[]
. Si quieres que reaccione a un cambio, incluye la dependencia[otraVariable]
.
- Solución: Añade un array de dependencias. Si quieres que se ejecute solo una vez, que sea
-
Error: Datos "viejos" (stale state) por dependencias faltantes.
// MAL ❌ const [count, setCount] = useState(0); useEffect(() => { // Este intervalo siempre "verá" count como 0, porque la función // del intervalo se creó en el primer render y nunca se actualizó. const intervalId = setInterval(() => { console.log(`El contador es: ${count}`); }, 1000); return () => clearInterval(intervalId); }, []); // El array vacío le dice a React: "nunca vuelvas a ejecutar esto"
-
Solución 1: Añadir la dependencia.
// BIEN ✅ (pero recrea el intervalo cada vez que count cambia) useEffect(() => { ... }, [count]);
-
Solución 2: Usar la actualización funcional (mejor en este caso).
// EXCELENTE ✅ // Dentro de `setCount`, React siempre te da el valor más reciente. // No necesitamos `count` como dependencia. useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // usamos la versión más reciente del estado }, 1000); return () => clearInterval(intervalId); }, []);
-
🚀 Retos prácticos
- Temporizador con Start/Stop/Reset: Usa
useEffect
yuseState
para crear un cronómetro funcional.
💻 Código base (haz clic para mostrar)
import React, { useState } from "react";
// Reto: Seguir el ratón
// Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.
// Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener.
interface Coordenadas {
x: number;
y: number;
}
export default function SeguirRatonReto(): React.ReactElement {
const [coordenadas, setCoordenadas] = useState<Coordenadas>({ x: 0, y: 0 });
// TODO: Implementar useEffect para:
// 1. Agregar un event listener para 'mousemove' en window
// 2. Actualizar las coordenadas cuando el ratón se mueva
// 3. Limpiar el event listener cuando el componente se desmonte
return (
<div style={{ padding: "20px", height: "100vh" }}>
<h1>Seguimiento del Ratón</h1>
<p style={{ fontSize: "1.5rem" }}>
Coordenadas: X: {coordenadas.x}, Y: {coordenadas.y}
</p>
<p style={{ color: "#666" }}>
Mueve el ratón para ver las coordenadas actualizarse en tiempo real.
</p>
</div>
);
}
- Seguir el ratón Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.
(Pista: window.addEventListener('mousemove', ...)
, y no olvides limpiar el listener).
💻 Código base (haz clic para mostrar)
import React, { useState } from "react";
// Reto: Seguir el ratón
// Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.
// Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener.
interface Coordenadas {
x: number;
y: number;
}
export default function SeguirRatonReto(): React.ReactElement {
const [coordenadas, setCoordenadas] = useState<Coordenadas>({ x: 0, y: 0 });
// TODO: Implementar useEffect para:
// 1. Agregar un event listener para 'mousemove' en window
// 2. Actualizar las coordenadas cuando el ratón se mueva
// 3. Limpiar el event listener cuando el componente se desmonte
return (
<div style={{ padding: "20px", height: "100vh" }}>
<h1>Seguimiento del Ratón</h1>
<p style={{ fontSize: "1.5rem" }}>
Coordenadas: X: {coordenadas.x}, Y: {coordenadas.y}
</p>
<p style={{ color: "#666" }}>
Mueve el ratón para ver las coordenadas actualizarse en tiempo real.
</p>
</div>
);
}
- Título de la página dinámico: Haz que el título del documento (
document.title
) se actualice para mostrar el valor de un contador o un input.
💻 Código base (haz clic para mostrar)
import React, { useState } from "react";
// Haz que el título del documento (document.title) se actualice para mostrar
// el valor de un contador o un input.
export default function TituloDinamicoReto(): React.ReactElement {
const [contador, setContador] = useState<number>(0);
const [textoInput, setTextoInput] = useState<string>("");
// TODO: Implementar useEffect para actualizar document.title
// El título debe cambiar cuando el contador o el input cambien
// Ejemplo: "Contador: 5" o "Texto: Hola mundo"
const incrementar = () => {
setContador(contador + 1);
};
const decrementar = () => {
setContador(contador - 1);
};
const reiniciar = () => {
setContador(0);
};
return (
<div style={{ padding: "20px" }}>
<h1>Título Dinámico</h1>
<div style={{ marginBottom: "30px" }}>
<h2>Contador: {contador}</h2>
<button onClick={incrementar} style={{ margin: "0 5px" }}>
+
</button>
<button onClick={decrementar} style={{ margin: "0 5px" }}>
-
</button>
<button onClick={reiniciar} style={{ margin: "0 5px" }}>
Reset
</button>
</div>
<div>
<h2>Input de Texto</h2>
<input
type="text"
value={textoInput}
onChange={(e) => setTextoInput(e.target.value)}
placeholder="Escribe algo..."
style={{ padding: "8px", fontSize: "16px", width: "300px" }}
/>
</div>
<p style={{ marginTop: "20px", color: "#666" }}>
Mira el título de la pestaña del navegador para ver los cambios.
</p>
</div>
);
}
- Autoguardado: Crea un
textarea
que guarde su contenido enlocalStorage
automáticamente 2 segundos después de que el usuario deje de escribir.
💻 Código base (haz clic para mostrar)
import React, { useState } from "react";
// Crea un textarea que guarde su contenido en localStorage automáticamente
// 2 segundos después de que el usuario deje de escribir.
export default function AutoguardadoReto(): React.ReactElement {
const [texto, setTexto] = useState<string>("");
const [ultimoGuardado, setUltimoGuardado] = useState<Date | null>(null);
// TODO: Implementar useEffect para autoguardar
// 1. Cargar el texto guardado de localStorage al montar el componente
// 2. Usar un setTimeout que se active 2 segundos después de que el usuario deje de escribir
// 3. Guardar en localStorage con la clave "autoguardado-texto"
// 4. Actualizar la fecha del último guardado
// Pista: usar clearTimeout para cancelar el timeout anterior cuando el usuario sigue escribiendo
return (
<div style={{ padding: "20px", maxWidth: "600px" }}>
<h1>Autoguardado en localStorage</h1>
<textarea
value={texto}
onChange={(e) => setTexto(e.target.value)}
placeholder="Escribe algo... Se guardará automáticamente 2 segundos después de que dejes de escribir."
style={{
width: "100%",
height: "200px",
padding: "10px",
fontSize: "16px",
border: "1px solid #ccc",
borderRadius: "4px",
resize: "vertical"
}}
/>
<div style={{ marginTop: "10px", color: "#666" }}>
{ultimoGuardado && (
<p>Último guardado: {ultimoGuardado.toLocaleTimeString()}</p>
)}
<p>Caracteres: {texto.length}</p>
</div>
</div>
);
}
Solución reto 1
🚀 Ver el código interactivo en CodeSandbox Haz clic para abrir el sandbox y experimentar con el código
💡 Tip: Puedes modificar el código en el editor de la izquierda y ver los cambios en tiempo real.
Top comments (0)