Estructurar el estado para que no te pelee
¿Por qué mis bugs de estado siempre aparecen en el mismo lugar?
Al terminar vas a poder explicar
- Detectar estado redundante y derivarlo durante el render
- Aplanar estado anidado y explicar por qué conviene
- Elegir dónde vive cada pedazo de estado: local, elevado o afuera
Hay una categoría entera de bugs que la gente atribuye a React y no son de React. Son estados que nunca deberían haber existido. Un dato guardado dos veces que se desincroniza. Un booleano que contradice a otro. Una lista anidada donde actualizar un elemento requiere tres niveles de copias.
No hace falta una librería para arreglar esto. Hacen falta cuatro decisiones.
1. Lo que cambia junto, va junto
Si dos useState se actualizan siempre en el mismo manejador, no son dos estados: son uno mal partido. Y partido tiene un costo real, porque cualquiera de los dos puede quedar atrás.
1// Dos estados que en la práctica son uno solo.2const [x, setX] = useState(0);3const [y, setY] = useState(0);4 5// Un estado que refleja lo que realmente es: una posición.6const [posicion, setPosicion] = useState({ x: 0, y: 0 });La contra: ahora tenés que copiar el objeto para actualizarlo. Vale la pena cuando los campos de verdad viajan juntos, y no vale cuando cambian por separado. El criterio es si se actualizan en la misma línea de código, no si «son parecidos».
2. Nada redundante: si se puede calcular, se calcula
Este es el error más caro de todos, y el más común. Si un valor se puede derivar de otro estado o de las props, no es estado.
1// ✗ nombreCompleto es un tercer estado que hay que mantener sincronizado.2const [nombre, setNombre] = useState("");3const [apellido, setApellido] = useState("");4const [nombreCompleto, setNombreCompleto] = useState("");5 6// ✓ Se calcula en cada render. Es imposible que quede desactualizado.7const [nombre, setNombre] = useState("");8const [apellido, setApellido] = useState("");9const nombreCompleto = `${nombre} ${apellido}`;«¿Pero no es caro recalcular en cada render?» Una concatenación cuesta nanosegundos. Un estado desincronizado cuesta una tarde de depuración. Y si el cálculo es caro de verdad, para eso está useMemo — que sigue siendo derivar, no duplicar.
3. Nada contradictorio
Si dos booleanos nunca pueden ser true a la vez, no son dos booleanos: son un estado con tres valores posibles.
1// ✗ Cuatro combinaciones, dos de ellas imposibles.2const [enviando, setEnviando] = useState(false);3const [enviado, setEnviado] = useState(false);4 5// ✓ Tres estados, y son exactamente los tres que existen.6const [estado, setEstado] = useState("listo"); // "listo" | "enviando" | "enviado"Con la primera versión, algún día vas a olvidarte de un setEnviando(false) y vas a tener un botón trabado para siempre. Con la segunda eso no se puede escribir.
4. Nada demasiado anidado
El estado profundamente anidado es incómodo de actualizar porque hay que copiar cada nivel. La salida es aplanarlo: guardar por id y referenciar.
1// ✗ Para marcar una tarea hay que copiar proyecto, lista y tarea.2{ proyectos: [{ id: 1, listas: [{ id: 9, tareas: [{ id: 4, hecha: false }] }] }] }3 4// ✓ Plano: actualizar una tarea toca un solo objeto.5{6 tareasPorId: { 4: { id: 4, hecha: false, listaId: 9 } },7 listasPorId: { 9: { id: 9, tareaIds: [4], proyectoId: 1 } },8 proyectosPorId: { 1: { id: 1, listaIds: [9] } },9}Es la misma normalización que usa cualquier base de datos, y por la misma razón: los datos anidados se duplican y los duplicados se desincronizan.
Y la quinta, que no es de estructura: dónde vive
Antes de estructurar el estado, preguntate si tiene que estar ahí. Hay cuatro lugares posibles, en este orden de preferencia:
- Local al componente, si nadie más lo necesita. Siempre empezá acá.
- Elevado al ancestro común, si dos hermanos lo comparten. Y sólo hasta el ancestro común, ni un nivel más arriba.
- En la URL, si el usuario esperaría poder compartir el enlace o usar el botón de atrás. Filtros, pestañas, paginado: casi siempre van acá.
- Afuera de React, en un store, si es global de verdad y cambia seguido.
Subir estado más alto de lo necesario es el motivo número uno de re-renders innecesarios — más que cualquier falta de memo. Antes de memoizar, fijate si el estado puede bajar.
Antes de seguir · predecí
Tenés una lista de productos y un campo de búsqueda. ¿Cómo guardás los resultados filtrados?
1const [productos, setProductos] = useState([]);2const [busqueda, setBusqueda] = useState("");3// ¿y los filtrados?Lo que te llevás
Agrupá lo que cambia junto. Derivá todo lo que se pueda derivar. Hacé imposibles los estados contradictorios. Aplaná lo anidado. Y antes que todo eso: preguntate si el estado tiene que vivir ahí. La mayoría de los bugs de estado son bugs de diseño, no de React.
Antes de marcarla, comprobá
- ¿Podés explicar detectar estado redundante y derivarlo durante el render?
- ¿Podés explicar aplanar estado anidado y explicar por qué conviene?
- ¿Podés explicar elegir dónde vive cada pedazo de estado: local, elevado o afuera?