Estado: la fuente de verdad local
En React, el estado representa datos que cambian en el tiempo y que afectan lo que se renderiza. Piensa en el estado como la fuente de verdad local de un componente: cuando el estado cambia, React vuelve a ejecutar el render del componente para reflejar ese cambio en la UI.
El objetivo no es “guardar todo en estado”, sino modelar el mínimo estado necesario para que la interfaz sea consistente y fácil de mantener.
¿Qué debe ser estado y qué no?
Antes de crear un useState, clasifica el dato en una de estas categorías:
- Constante: no cambia durante la vida del componente (o viene de un módulo). Ej.: lista fija de países, configuración de columnas.
- Derivado: se puede calcular a partir de props o estado. No lo guardes en estado; calcúlalo en el render (o memorízalo si es costoso).
- Estado: cambia por interacción del usuario, por eventos del sistema o por respuestas de red, y necesitas que el render lo refleje.
Regla práctica: si puedes obtener un valor con una función pura a partir de otros valores que ya tienes, probablemente es derivado.
Ejemplo: estado vs. derivado
import { useState } from "react";export function CartSummary({ items }) { // items viene por props const total = items.reduce((acc, it) => acc + it.price * it.qty, 0); // derivado return <p>Total: ${total}</p>;}Aquí total no debe ser estado: se calcula desde items. Guardarlo en estado introduciría riesgo de desincronización.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Renderizado reactivo: qué pasa cuando cambia el estado
Cuando llamas al setter de estado (por ejemplo setCount), React programa una actualización. En el siguiente render, el componente se ejecuta de nuevo con el nuevo estado y React actualiza el DOM donde haga falta.
Dos ideas clave
- El render debe ser una función pura del estado y props: mismo input, mismo output.
- No leas el DOM para “saber” el estado. El estado vive en React; el DOM es una proyección.
Guía práctica: modelar estado mínimo (paso a paso)
Paso 1: lista lo que puede cambiar
Escribe los datos que cambian: texto de un input, ítem seleccionado, si un panel está abierto, datos cargados, errores, etc.
Paso 2: elimina lo que sea constante
Si no cambia, no es estado. Déjalo como constante o prop.
Paso 3: elimina lo derivado
Si se calcula desde otros valores, no lo guardes. Ej.: filteredItems se calcula desde items y query.
Paso 4: decide la forma del estado
Elige una estructura que haga fáciles las actualizaciones. Preferible: simple, normalizada, sin duplicación.
Paso 5: define actualizaciones inmutables
En React se actualiza estado de forma inmutable: no mutas el objeto/array existente; creas uno nuevo con los cambios.
Evitar inconsistencias: no dupliques estado
Un problema común es guardar el mismo “hecho” en dos sitios. Ejemplo típico: guardar items y también total en estado. Si actualizas uno y olvidas el otro, la UI queda inconsistente.
| Mal (duplicado) | Bien (única fuente de verdad) |
|---|---|
const [items, setItems] = useState([]); const [total, setTotal] = useState(0); | const [items, setItems] = useState([]); const total = items.reduce(...); |
Normalización básica de estructuras
Cuando el estado representa colecciones, suele ser más fácil mantenerlo si lo normalizas. En vez de arrays anidados difíciles de actualizar, separa:
- Diccionario por id para acceso/actualización rápida.
- Array de ids para orden.
const initial = { byId: { "p1": { id: "p1", name: "Teclado", price: 30 }, "p2": { id: "p2", name: "Mouse", price: 15 } }, allIds: ["p1", "p2"]};Esto reduce la duplicación y hace más predecibles las actualizaciones.
Actualización inmutable de objetos y arrays
Objetos: copiar y sobrescribir
const [profile, setProfile] = useState({ name: "Ana", city: "Lima" });function updateCity(newCity) { setProfile(prev => ({ ...prev, city: newCity }));}Arrays: crear un nuevo array
- Agregar:
[...prev, newItem] - Eliminar:
prev.filter(...) - Actualizar:
prev.map(...)
const [todos, setTodos] = useState([{ id: 1, text: "Leer", done: false }]);function toggleTodo(id) { setTodos(prev => prev.map(t => (t.id === id ? { ...t, done: !t.done } : t)) );}function removeTodo(id) { setTodos(prev => prev.filter(t => t.id !== id));}function addTodo(text) { setTodos(prev => [...prev, { id: crypto.randomUUID(), text, done: false }]);}Nota: usar la forma funcional setState(prev => ...) evita errores cuando varias actualizaciones ocurren seguidas.
Escenario 1: toggles (UI booleana)
Un toggle es el ejemplo más directo de estado: un booleano que controla el render.
import { useState } from "react";export function NewsletterToggle() { const [subscribed, setSubscribed] = useState(false); return ( <div> <label> <input type="checkbox" checked={subscribed} onChange={e => setSubscribed(e.target.checked)} /> Suscribirme </label> {subscribed && <p>Gracias por suscribirte.</p>} </div> );}Escenario 2: formularios (inputs controlados)
En formularios, el estado suele ser el valor del input. Un patrón común es un objeto form con campos, actualizado de forma inmutable.
Formulario simple paso a paso
- Define el estado inicial.
- Vincula
valueyonChange(input controlado). - Valida y envía usando el estado como fuente de verdad.
import { useState } from "react";export function SignupForm() { const [form, setForm] = useState({ email: "", password: "" }); const [error, setError] = useState(null); function onChange(e) { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); } function onSubmit(e) { e.preventDefault(); if (!form.email.includes("@")) { setError("Email inválido"); return; } setError(null); // enviar form.email y form.password } return ( <form onSubmit={onSubmit}> <input name="email" value={form.email} onChange={onChange} placeholder="Email" /> <input name="password" type="password" value={form.password} onChange={onChange} placeholder="Contraseña" /> {error && <p>{error}</p>} <button type="submit">Crear cuenta</button> </form> );}Evita guardar en estado cosas como “isEmailValid” si se puede derivar de form.email. Puedes calcularlo en el render:
const isEmailValid = form.email.includes("@");Escenario 3: selección en listas
Para selección, es común guardar el id seleccionado, no el objeto completo (evita duplicación y facilita sincronía si la lista cambia).
import { useState } from "react";export function ProductList({ products }) { const [selectedId, setSelectedId] = useState(null); const selected = products.find(p => p.id === selectedId) ?? null; // derivado return ( <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}> <ul> {products.map(p => ( <li key={p.id}> <button onClick={() => setSelectedId(p.id)}> {p.name} </button> </li> ))} </ul> <div> {selected ? ( <div> <h3>{selected.name}</h3> <p>Precio: ${selected.price}</p> </div> ) : ( <p>Selecciona un producto</p> )} </div> </div> );}Si guardas el objeto seleccionado completo en estado y luego products se actualiza desde fuera, puedes quedarte con una versión vieja. Guardar el id reduce ese riesgo.
Escenario 4: carga de datos (estado de red)
Cuando cargas datos, normalmente necesitas representar al menos: datos, estado de carga y error. Mantén el modelo simple y explícito.
import { useEffect, useState } from "react";export function Users() { const [users, setUsers] = useState([]); const [status, setStatus] = useState("idle"); // "idle" | "loading" | "success" | "error" const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function load() { setStatus("loading"); setError(null); try { const res = await fetch("/api/users"); if (!res.ok) throw new Error("Error HTTP"); const data = await res.json(); if (!cancelled) { setUsers(data); setStatus("success"); } } catch (e) { if (!cancelled) { setError(e.message); setStatus("error"); } } } load(); return () => { cancelled = true; }; }, []); if (status === "loading") return <p>Cargando...</p>; if (status === "error") return <p>{error}</p>; return ( <ul> {users.map(u => ( <li key={u.id}>{u.name}</li> ))} </ul> );}Evita estados redundantes como isLoading y status a la vez. Elige uno (por ejemplo status) para que no haya combinaciones inválidas.
Patrones para evitar errores comunes
1) No mutar estado existente
Esto es un error típico:
// NO: mutación directaconst [items, setItems] = useState([]);function add(item) { items.push(item); setItems(items);}La versión correcta crea un nuevo array:
function add(item) { setItems(prev => [...prev, item]);}2) Cuidado con “estado espejo”
“Estado espejo” es copiar una prop a estado sin necesidad. Si una prop ya es la fuente de verdad, duplicarla en estado suele causar desincronización. Si necesitas editar localmente un valor que viene de fuera (por ejemplo, un formulario de edición), define claramente el momento en que inicializas el estado local (por ejemplo, al abrir un modal) y evita mantener dos fuentes de verdad activas para lo mismo.
3) Mantén el estado lo más cercano posible a donde se usa
Si solo un componente necesita un dato, mantenlo local. Si varios componentes lo necesitan, sube la fuente de verdad al ancestro común (para no duplicar estado en ramas distintas).
4) Prefiere ids y datos normalizados para colecciones
Guardar objetos completos en varios lugares aumenta el riesgo de inconsistencias. Guardar ids y derivar el objeto desde una colección central suele ser más estable.