useState: estado local con actualizaciones inmutables
useState te permite guardar y actualizar estado dentro de un componente funcional. La idea clave es que el estado se trata como inmutable: en lugar de modificar un objeto/array existente, creas uno nuevo. Esto ayuda a que React detecte cambios y renderice de forma predecible.
Firma y reglas prácticas
const [valor, setValor] = useState(inicial)setValor(nuevoValor)reemplaza el estado.setValor(prev => siguiente)usa una función de actualización (recomendada cuando el siguiente estado depende del anterior).- No llames hooks dentro de condiciones o bucles; deben ejecutarse siempre en el mismo orden.
Paso a paso: contador con actualización segura
Cuando el nuevo valor depende del anterior (por ejemplo, incrementar), usa la forma funcional para evitar problemas con actualizaciones agrupadas (batching) o eventos rápidos.
import { useState } from "react";export function Counter() { const [count, setCount] = useState(0); function inc() { setCount(prev => prev + 1); } function dec() { setCount(prev => prev - 1); } return ( <div> <p>Count: {count}</p> <button onClick={dec}>-</button> <button onClick={inc}>+</button> </div> );}Actualizaciones inmutables con objetos
Si tu estado es un objeto, evita mutarlo directamente. Crea una copia con ... y cambia solo lo necesario.
const [profile, setProfile] = useState({ name: "Ada", email: "ada@dev.com" });function onNameChange(e) { const nextName = e.target.value; setProfile(prev => ({ ...prev, name: nextName }));}Actualizaciones inmutables con arrays
Para agregar, eliminar o editar elementos, usa métodos que devuelvan un nuevo array (map, filter, spread).
const [todos, setTodos] = useState([{ id: 1, text: "Leer" }]);function addTodo(text) { setTodos(prev => [...prev, { id: Date.now(), text }]);}function removeTodo(id) { setTodos(prev => prev.filter(t => t.id !== id));}function renameTodo(id, text) { setTodos(prev => prev.map(t => (t.id === id ? { ...t, text } : t)));}Error frecuente: mutar y luego “setear” el mismo objeto
Este patrón es problemático porque mantiene la misma referencia y puede causar renders inconsistentes.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
// Evitar: mutación directaconst [settings, setSettings] = useState({ dark: false });function toggle() { settings.dark = !settings.dark; // muta setSettings(settings); // misma referencia}En su lugar:
function toggle() { setSettings(prev => ({ ...prev, dark: !prev.dark }));}useEffect: sincronizar con el mundo externo
useEffect sirve para ejecutar efectos secundarios: trabajo que “sale” del render y sincroniza tu componente con algo externo (red, timers, suscripciones, APIs del navegador como localStorage). Un efecto se ejecuta después de que React pinta la UI.
Modelo mental: render vs efecto
- El render debe ser puro: dado el mismo estado/props, produce el mismo UI.
- El efecto es para sincronización: “cuando X cambie, haz Y afuera”.
- Si algo puede calcularse a partir de estado/props, normalmente NO es un efecto (es cálculo derivado).
Sintaxis y dependencias
useEffect(() => { // efecto return () => { // cleanup (opcional) };}, [dep1, dep2]);| Dependencias | Cuándo corre | Uso típico |
|---|---|---|
[] | Al montar (y cleanup al desmontar) | Inicialización, cargar datos una vez, suscribirse |
[a] | Al montar y cuando a cambie | Sincronizar con un valor específico |
| (sin array) | En cada render | Raro; suele indicar un problema o falta de dependencias |
Cleanup: limpiar timers y suscripciones
Si tu efecto crea algo que debe deshacerse (timer, listener, suscripción), devuelve una función de limpieza. React la ejecuta antes de re-ejecutar el efecto y al desmontar.
import { useEffect, useState } from "react";export function Clock() { const [now, setNow] = useState(() => new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); return <p>{now.toLocaleTimeString()}</p>;}Patrones esenciales con useEffect
1) Cargar datos al montar (fetch) con manejo de estados
Patrón típico: loading, error, data. Además, evita actualizar estado si el componente se desmonta o si llega una respuesta vieja.
import { useEffect, useState } from "react";export function UsersList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); async function load() { try { setLoading(true); setError(null); const res = await fetch("/api/users", { signal: controller.signal }); if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); setUsers(data); } catch (e) { if (e.name !== "AbortError") setError(e); } finally { setLoading(false); } } load(); return () => controller.abort(); }, []); if (loading) return <p>Cargando...</p>; if (error) return <p>Error: {String(error.message || error)}</p>; return ( <ul> {users.map(u => (<li key={u.id}>{u.name}</li>))} </ul> );}2) Reaccionar a cambios específicos
Cuando un valor cambia (por ejemplo, un término de búsqueda), ejecutas un efecto que sincroniza con el exterior. Aquí el array de dependencias es el contrato: el efecto depende de query, así que debe incluirlo.
import { useEffect, useState } from "react";export function SearchUsers() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); useEffect(() => { if (query.trim() === "") { setResults([]); return; } const controller = new AbortController(); async function run() { const res = await fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal: controller.signal, }); const data = await res.json(); setResults(data); } run().catch(() => {}); return () => controller.abort(); }, [query]); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <ul>{results.map(r => (<li key={r.id}>{r.name}</li>))}</ul> </div> );}3) Sincronizar con localStorage
Un caso común es persistir una preferencia. La lectura inicial puede hacerse en el inicializador de useState (para evitar leer en cada render) y la escritura en un efecto que depende del valor.
import { useEffect, useState } from "react";export function ThemeToggle() { const [theme, setTheme] = useState(() => { return localStorage.getItem("theme") || "light"; }); useEffect(() => { localStorage.setItem("theme", theme); }, [theme]); return ( <button onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}> Theme: {theme} </button> );}4) Separar efectos por responsabilidad
Si un componente hace varias sincronizaciones (por ejemplo, título del documento y analytics), es mejor tener efectos separados. Esto reduce dependencias innecesarias y hace más fácil razonar sobre el cleanup.
useEffect(() => { document.title = `Perfil: ${name}`;}, [name]);useEffect(() => { analytics.track("profile_view", { userId });}, [userId]);Cómo evitar bucles de render y otros errores frecuentes
Dependencias omitidas
Si usas variables del scope (props/estado/funciones) dentro del efecto, normalmente deben estar en el array de dependencias. Omitirlas puede causar que el efecto use valores “viejos” (stale) y se desincronice.
// Problema: usa "query" pero no está en dependenciasuseEffect(() => { fetch(`/api?q=${query}`);}, []);Arreglo:
useEffect(() => { fetch(`/api?q=${query}`);}, [query]);Efectos que actualizan estado sin control (bucle)
Si un efecto hace setState y ese estado está en sus dependencias, puedes crear un ciclo infinito. Pregunta: ¿estoy actualizando algo que a su vez dispara el mismo efecto sin una condición?
// Bucle: cada vez que "count" cambia, lo incrementa otra vezuseEffect(() => { setCount(count + 1);}, [count]);Soluciones típicas:
- Si quieres que ocurra una sola vez, usa
[]. - Si depende de un evento externo, mueve la actualización al manejador del evento.
- Si necesitas derivar un valor, no uses efecto: calcula en render o usa memoización.
Confundir efecto con cálculo derivado
Si un valor se puede obtener a partir de estado/props, no lo guardes en estado con un efecto, porque introduces duplicación y riesgo de desincronización.
// Evitar: estado derivado + efectoconst [fullName, setFullName] = useState("");useEffect(() => { setFullName(first + " " + last);}, [first, last]);Mejor: calcular directamente.
const fullName = first + " " + last;Dependencias que cambian por identidad (objetos/funciones inline)
Si pones un objeto o función creada en cada render dentro de dependencias, el efecto se disparará siempre. En lugar de eso, mueve la creación dentro del efecto o estabiliza referencias (por ejemplo, con useMemo/useCallback en capítulos posteriores si aplica).
// Se recrea en cada render, dispara el efecto siempreconst options = { headers: { "X-App": "demo" } };useEffect(() => { fetch("/api", options);}, [options]);Alternativa simple: crear dentro del efecto.
useEffect(() => { const options = { headers: { "X-App": "demo" } }; fetch("/api", options);}, []);