📋 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 -
useMemoyuseCallback– 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
setIntervalosetTimeout.
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 deluseEffecto 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
useEffectyuseStatepara 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
textareaque guarde su contenido enlocalStorageautomá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)