Fuente de verdad: un solo lugar para cada dato
En React, un dato que cambia debería tener una fuente de verdad: un único estado que lo representa. Cuando el mismo concepto aparece en varios estados (duplicado), empiezan los problemas: inconsistencias, bugs “fantasma” y lógica extra para mantener todo sincronizado.
Señales de que duplicaste estado
- Actualizas un estado y “olvidas” actualizar otro.
- Dos componentes muestran el “mismo” dato pero a veces difieren.
- Necesitas
useEffectsolo para copiar valores entre estados.
Diagrama conceptual: fuente de verdad y derivaciones
[Estado (fuente de verdad)] --(render)--> [UI] --(eventos)--> [setState] --> [Estado]Idea clave: si un valor puede calcularse a partir de otro, normalmente no merece un estado propio; es una derivación.
Derivaciones: calcula en render en lugar de guardar
Una derivación es un valor que se obtiene a partir de props/estado actuales: totales, filtros, conteos, validaciones, textos formateados, etc. Guardar derivaciones en estado suele crear duplicación.
Ejemplo: total y validación derivados
Supón un carrito con cantidades. En vez de tener total en estado, calcúlalo desde items.
function CartSummary({ items }) { const total = items.reduce((acc, item) => acc + item.price * item.qty, 0); const isEmpty = items.length === 0; return ( <section> <p>Total: ${total}</p> {isEmpty ? <p>Tu carrito está vacío</p> : null} </section> );}Esto mantiene el componente previsible: el total siempre coincide con los items actuales.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Guía práctica: ¿esto debe ser estado o derivación?
| Pregunta | Si la respuesta es “sí” | Entonces |
|---|---|---|
| ¿Se puede calcular a partir de props/estado existentes? | Sí | Derívalo en render (o en una función auxiliar). |
| ¿Representa algo que el usuario puede cambiar directamente? | Sí | Probablemente es estado. |
| ¿Necesitas conservarlo entre renders aunque las entradas cambien? | Sí | Probablemente es estado (o un efecto/almacenamiento externo). |
Props hacia abajo, eventos hacia arriba
Cuando un componente hijo necesita “pedir” un cambio, no modifica el estado del padre directamente. En su lugar, el padre pasa una función (callback) y el hijo la ejecuta ante un evento.
Diagrama conceptual: datos bajan, eventos suben
Padre (estado) | props (datos, callbacks) vHijo (UI) | onClick/onChange llama callback vPadre actualiza estadoEste patrón mantiene el control del estado donde vive la fuente de verdad, y hace que los hijos sean más reutilizables.
Elevación de estado: cuando varios componentes lo necesitan
Si dos o más componentes necesitan leer y/o actualizar el mismo dato, ese estado debe vivir en su ancestro común más cercano. A esto se le llama elevar estado.
Diagrama conceptual para decidir ubicación del estado
Ancestro común (¿quién necesita el dato?) / \ Componente A Componente B (lee/actualiza) (lee/actualiza)Regla práctica: coloca el estado en el componente más bajo del árbol que aún pueda abastecer a todos los consumidores necesarios.
Checklist para ubicar el estado
- ¿Quién lo muestra? (componentes que lo leen)
- ¿Quién lo cambia? (componentes que disparan eventos)
- ¿Cuál es el ancestro común más cercano entre lectores y escritores?
- ¿Se puede dividir en estados independientes? (evita “mega-estados” innecesarios)
Refactor 1: de estados duplicados a uno único (elevación)
Escenario: un buscador y una lista necesitan compartir el texto de búsqueda. Un error típico es que cada uno tenga su propio estado, quedando desincronizados.
Antes (duplicado): cada componente con su propio estado
function SearchBox() { const [query, setQuery] = useState(""); return <input value={query} onChange={(e) => setQuery(e.target.value)} />;}function ResultsList({ items }) { const [query, setQuery] = useState(""); const filtered = items.filter((x) => x.name.includes(query)); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{filtered.map((x) => <li key={x.id}>{x.name}</li>)}</ul> </div> );}Problema: hay dos query distintos. Cambiar uno no afecta al otro.
Después (único): estado en el ancestro común
function SearchPage({ items }) { const [query, setQuery] = useState(""); const filtered = items.filter((x) => x.name.toLowerCase().includes(query.toLowerCase())); return ( <div> <SearchBox query={query} onQueryChange={setQuery} /> <ResultsList items={filtered} /> </div> );}function SearchBox({ query, onQueryChange }) { return ( <input value={query} onChange={(e) => onQueryChange(e.target.value)} placeholder="Buscar..." /> );}function ResultsList({ items }) { return <ul>{items.map((x) => <li key={x.id}>{x.name}</li>)}</ul>;}Ahora hay una sola fuente de verdad (query) y la lista siempre refleja el input.
Paso a paso para aplicar elevación de estado
- Identifica el dato compartido (por ejemplo,
query). - Encuentra el ancestro común más cercano de los componentes que lo usan.
- Mueve el
useStatea ese ancestro. - Pasa el valor hacia abajo por props.
- Pasa callbacks hacia abajo para que los hijos puedan solicitar cambios.
- Elimina estados duplicados en los hijos.
Refactor 2: derivar valores en render en vez de sincronizarlos
Otro caso común: guardar en estado un valor que es calculable, y luego “sincronizarlo” con efectos. Eso añade complejidad y riesgo de bucles o desajustes.
Antes (sincronización innecesaria)
function PricePanel({ price, qty }) { const [total, setTotal] = useState(0); useEffect(() => { setTotal(price * qty); }, [price, qty]); return <p>Total: ${total}</p>;}Después (derivación directa)
function PricePanel({ price, qty }) { const total = price * qty; return <p>Total: ${total}</p>;}Menos piezas móviles: el total siempre es correcto y no depende de sincronización.
Refactor 3: mover lógica fuera del JSX para mejorar legibilidad
Cuando el JSX acumula condiciones, mapeos y formateos, se vuelve difícil de leer. Una mejora simple es extraer cálculos y decisiones a variables o funciones auxiliares.
Antes (JSX cargado)
function UserBadge({ user }) { return ( <div> <h3> {user.firstName} {user.lastName} {user.isPro ? " (PRO)" : ""} </h3> <p> {user.lastLogin ? `Último acceso: ${new Date(user.lastLogin).toLocaleDateString()}` : "Nunca ha iniciado sesión"} </p> </div> );}Después (cálculos arriba, JSX declarativo)
function UserBadge({ user }) { const fullName = `${user.firstName} ${user.lastName}`; const proLabel = user.isPro ? " (PRO)" : ""; const lastLoginText = user.lastLogin ? `Último acceso: ${new Date(user.lastLogin).toLocaleDateString()}` : "Nunca ha iniciado sesión"; return ( <div> <h3>{fullName}{proLabel}</h3> <p>{lastLoginText}</p> </div> );}El JSX queda como una “plantilla” clara, y la lógica queda en un bloque fácil de testear mentalmente.
Separar lógica de negocio simple en funciones auxiliares
Una forma práctica de mantener componentes previsibles es extraer lógica pura (sin efectos, sin acceso al DOM, sin mutaciones) a funciones auxiliares. Esto reduce el ruido en el componente y facilita reutilización.
Criterios para extraer a una función
- Es una transformación pura: mismo input, mismo output (por ejemplo, filtrar, ordenar, formatear).
- Se repite en más de un componente.
- Hace el render más legible al sacar detalles del JSX.
- No necesita estado: solo usa argumentos.
Ejemplo: filtrar y ordenar productos con funciones puras
function normalize(text) { return text.trim().toLowerCase();}function filterProducts(products, query) { const q = normalize(query); if (!q) return products; return products.filter((p) => normalize(p.name).includes(q));}function sortProductsByPrice(products, direction) { const factor = direction === "asc" ? 1 : -1; return [...products].sort((a, b) => (a.price - b.price) * factor);}function ProductsPage({ products }) { const [query, setQuery] = useState(""); const [direction, setDirection] = useState("asc"); const visible = sortProductsByPrice(filterProducts(products, query), direction); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <select value={direction} onChange={(e) => setDirection(e.target.value)}> <option value="asc">Precio: menor a mayor</option> <option value="desc">Precio: mayor a menor</option> </select> <ul> {visible.map((p) => ( <li key={p.id}>{p.name} - ${p.price}</li> ))} </ul> </div> );}Observa que el componente solo orquesta estado y render; la lógica de negocio simple vive en funciones puras.
Mapa mental de decisiones: estado, derivación o prop
¿El dato cambia con interacción o tiempo? ├─ No: probablemente es prop o constante └─ Sí: ¿lo usan varios componentes? ├─ No: estado local en el componente que lo usa └─ Sí: elevar al ancestro común más cercano¿Se puede calcular desde otro estado/prop? ├─ Sí: derivación (no guardarlo en estado) └─ No: estado (fuente de verdad)Aplicando estas reglas, reduces duplicación, mantienes un flujo de datos claro y haces que los componentes sean más fáciles de mantener.