Cómo leer un error en React: del síntoma al concepto
Muchos fallos en React se repiten porque el síntoma (algo “se ve mal”) no apunta directamente a la causa (un concepto roto). En este capítulo vas a diagnosticar con un patrón fijo: síntoma observable → causa conceptual → ejemplo mínimo reproducible → solución. La meta no es memorizar “trucos”, sino identificar qué regla conceptual se está violando.
1) Mutación de estado (o de objetos/arrays dentro del estado)
Síntoma observable
- La UI no se actualiza después de “cambiar” algo.
- Se actualiza a veces, o se actualiza otra parte inesperada.
- Un componente hijo parece “no enterarse” del cambio.
Causa conceptual
React decide si re-renderiza comparando referencias. Si mutas un objeto/array y vuelves a guardar la misma referencia, React puede no detectar el cambio. Además, mutar rompe la idea de “instantáneas” de estado: el pasado y el presente se mezclan.
Ejemplo mínimo reproducible
import { useState } from "react"; export default function App() { const [items, setItems] = useState([{ id: 1, done: false }]); function toggleFirst() { items[0].done = !items[0].done; setItems(items); } return ( <div> <button onClick={toggleFirst}>Toggle</button> <pre>{JSON.stringify(items, null, 2)}</pre> </div> ); }Solución
Actualiza de forma inmutable: crea nuevas referencias para lo que cambia (y conserva lo que no cambia).
function toggleFirst() { setItems(prev => prev.map((it, idx) => idx === 0 ? { ...it, done: !it.done } : it )); }Guía práctica:
- Si el estado es un
array, usamap,filter,sliceo spread ([...prev]). - Si el estado es un
object, usa spread ({...prev, x: nuevo}) o copia profunda solo donde haga falta. - Si el cambio depende del estado anterior, usa el patrón funcional:
setX(prev => ...).
2) Keys inestables (identidad que cambia entre renders)
Síntoma observable
- Al escribir en un input dentro de una lista, el foco “salta” o se pierde.
- Se mezclan valores entre filas (“lo que escribí en una fila aparece en otra”).
- Animaciones o estados locales de ítems se reasignan a otro ítem.
Causa conceptual
Las key definen identidad para la reconciliación. Si la key cambia cuando cambia el orden o el contenido, React reutiliza nodos equivocados o remonta componentes, perdiendo estado local.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Ejemplo mínimo reproducible
import { useState } from "react"; export default function App() { const [names, setNames] = useState(["Ada", "Linus", "Grace"]); return ( <div> <button onClick={() => setNames(prev => [...prev].reverse())}>Reverse</button> {names.map((n, i) => ( <div key={i}> <input defaultValue={n} /> </div> ))} </div> ); }Solución
Usa una key estable derivada de un identificador persistente (id). Si no existe, créalo al momento de crear el ítem (no en el render).
const [people, setPeople] = useState([ { id: "p1", name: "Ada" }, { id: "p2", name: "Linus" }, { id: "p3", name: "Grace" }, ]); {people.map(p => ( <div key={p.id}> <input defaultValue={p.name} /> </div> ))}Regla práctica: evita key={index} si la lista puede reordenarse, filtrarse, insertarse en medio o si cada ítem tiene estado local.
3) Renderizar valores no válidos (o asumir que siempre hay datos)
Síntoma observable
- Error:
Objects are not valid as a React child. - La pantalla queda en blanco por un crash al intentar acceder a
undefined. - Se ve
[object Object]o contenido inesperado.
Causa conceptual
En JSX solo puedes renderizar tipos “renderizables”: strings, numbers, elementos React, arrays de renderizables, null/undefined/false (no renderizan). Un objeto plano no es renderizable. Además, los datos pueden llegar tarde (carga) o incompletos.
Ejemplo mínimo reproducible
export default function App() { const user = { name: "Ada" }; return <div>{user}</div>; }Solución
Renderiza una propiedad, transforma el objeto o serialízalo para depurar.
return <div>{user.name}</div>; // o para debug: <pre>{JSON.stringify(user, null, 2)}</pre>Guía práctica para datos opcionales:
- Usa guardas:
{user ? user.name : "Cargando..."}. - Usa encadenamiento opcional:
user?.profile?.emailcuando aplique. - Si renderizas listas, asegura que sea un array:
(items ?? []).map(...).
4) Efectos mal definidos (dependencias incorrectas, loops, o trabajo en el lugar equivocado)
Síntoma observable
- El efecto se ejecuta “infinitamente” (loop de renders).
- El efecto no se ejecuta cuando debería (datos desactualizados).
- Warnings de dependencias o comportamiento inconsistente entre renders.
Causa conceptual
useEffect se ejecuta después del render y se re-ejecuta cuando cambian sus dependencias. Si dentro del efecto actualizas estado que está en las dependencias (directa o indirectamente), puedes crear un ciclo. Si omites dependencias, el efecto “captura” valores viejos (stale closure). Además, hay lógica que no debería ser un efecto (por ejemplo, derivar datos que se pueden calcular en render).
Ejemplo mínimo reproducible
import { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); }, [count]); return <div>{count}</div>; }Solución
Define el efecto por “sincronización” (con el mundo externo) y controla dependencias. Si quieres un incremento único al montar, no dependas de count.
useEffect(() => { setCount(c => c + 1); }, []);Si el efecto depende de props/estado, inclúyelos y evita actualizar lo mismo sin condición.
useEffect(() => { if (!query) return; let cancelled = false; (async () => { const res = await fetch(`/api?q=${encodeURIComponent(query)}`); const data = await res.json(); if (!cancelled) setResults(data); })(); return () => { cancelled = true; }; }, [query]);Guía práctica paso a paso para depurar efectos:
- Escribe en una frase qué sincroniza: “cuando cambie X, quiero reflejarlo en Y externo”.
- Lista variables usadas dentro del efecto y verifica que estén en dependencias.
- Si hay
setState, pregunta: ¿esto cambia una dependencia del mismo efecto? Si sí, añade una condición o replantea el diseño. - Si el efecto solo calcula un valor derivado, muévelo al render (o memoización si es costoso).
5) Handlers mal enlazados (ejecución inmediata o referencia incorrecta)
Síntoma observable
- Una acción ocurre al renderizar, sin hacer clic.
- El handler recibe un valor incorrecto o siempre el mismo.
- Errores como “Cannot update a component while rendering a different component”.
Causa conceptual
En JSX debes pasar una función como handler. Si llamas la función en el render, se ejecuta inmediatamente. También es común capturar valores equivocados si se construye mal la función o se confunde el evento con un argumento.
Ejemplo mínimo reproducible
export default function App() { function sayHi(name) { alert("Hola " + name); } return <button onClick={sayHi("Ada")}>Saludar</button>; }Solución
Pasa una función que, al ejecutarse, llame a tu lógica.
<button onClick={() => sayHi("Ada")}>Saludar</button>Si necesitas el evento y un argumento:
<button onClick={(e) => { e.preventDefault(); sayHi("Ada"); }}>Saludar</button>Checklist rápida:
- Si ves paréntesis en el JSX (
onClick={fn()}), sospecha ejecución inmediata. - Verifica si el handler espera
(event)o tus propios argumentos. - Si el handler actualiza estado, asegúrate de no hacerlo durante el render.
6) Estado derivado duplicado (dos fuentes de verdad)
Síntoma observable
- Valores que deberían coincidir se desincronizan.
- Arreglar un bug “rompe” otro porque hay que actualizar dos estados.
- Se agregan efectos para “mantener en sync” estados que se derivan.
Causa conceptual
Guardar en estado algo que puede calcularse a partir de props/estado existente crea duplicación. Eso introduce inconsistencias porque ahora hay que actualizar múltiples lugares en cada cambio.
Ejemplo mínimo reproducible
import { useEffect, useState } from "react"; export default function App({ price, qty }) { const [total, setTotal] = useState(price * qty); useEffect(() => { setTotal(price * qty); }, [price, qty]); return <div>Total: {total}</div>; }Solución
No guardes total en estado si es derivable: calcúlalo en el render.
export default function App({ price, qty }) { const total = price * qty; return <div>Total: {total}</div>; }Si el cálculo es costoso, memoízalo (sin convertirlo en “estado duplicado”).
import { useMemo } from "react"; const total = useMemo(() => expensiveCalc(price, qty), [price, qty]);Regla práctica: si puedes recomputarlo a partir de la fuente de verdad en cada render, no lo guardes como estado.
Checklist de depuración orientada a conceptos
1) ¿Qué cambia el estado?
- Localiza todos los
setStateque afectan al dato problemático. - Confirma si usas actualización funcional cuando depende del valor anterior:
setX(prev => ...). - Busca mutaciones:
push,splice, asignación directa a propiedades (obj.x =), ordenamientos in-place (sortsin copia).
2) ¿Qué props llegan realmente?
- Inspecciona el valor en el punto de uso:
<pre>{JSON.stringify(props, null, 2)}</pre>. - Verifica supuestos: ¿puede ser
nulloundefineddurante carga? - Si un hijo “no se actualiza”, revisa si el padre está pasando la misma referencia por mutación.
3) ¿Cuándo se ejecuta el efecto?
- Revisa dependencias: todo lo usado dentro del efecto debería estar en el array (salvo constantes externas estables).
- Si hay loops, identifica: efecto →
setState→ cambia dependencia → re-render → efecto. - Si hay valores desactualizados, sospecha closures por dependencias omitidas.
4) ¿Por qué se re-renderiza?
- Un render ocurre cuando cambia el estado local, cambian props, o cambia el contexto consumido.
- Si “se re-renderiza demasiado”, busca: estado duplicado, efectos que setean estado sin necesidad, o creación de objetos/funciones que disparan renders en hijos sensibles.
- Si “no re-renderiza”, busca: mutación (misma referencia), guardas que retornan antes de renderizar, o errores que rompen el árbol.
Tabla rápida: síntoma → sospecha principal
| Síntoma | Sospecha | Primer chequeo |
|---|---|---|
| UI no cambia tras actualizar | Mutación / misma referencia | ¿Usaste spread/map en vez de mutar? |
| Inputs en lista pierden foco | Keys inestables | ¿Key es index o cambia al reordenar? |
| Error “Objects are not valid…” | Render de objeto | ¿Estás renderizando un objeto plano? |
| Loop de renders | Efecto mal definido | ¿El efecto setea estado que es dependencia? |
| Acción ocurre al cargar | Handler ejecutado en render | ¿Hay onClick={fn()}? |
| Datos inconsistentes | Estado derivado duplicado | ¿Puedes calcularlo desde otra fuente? |