Estado normalizado: el store como base de datos
Guardar entidades en árboles anidados —el autor dentro del post, el post dentro del hilo— duplica datos y siembra bugs: el mismo usuario vive en veinte sitios y actualizarlo en uno deja copias rancias en los otros. La forma normalizada guarda cada entidad una sola vez, por id, como tablas de una base de datos, y conecta las relaciones por id en vez de por anidamiento. Esta lección explica las patologías del árbol anidado, la forma ids más entities, cómo se modelan las relaciones uno-a-muchos y muchos-a-muchos, por qué elimina anomalías de actualización y estabiliza referencias, el coste de desnormalizar al leer, y las herramientas de 2026 para llegar allí.
El estado de una aplicación tiende, si nadie lo impide, a copiar la forma de las respuestas del servidor: un hilo que contiene sus posts, cada post con su autor incrustado, cada autor con sus datos completos. Es cómodo de recibir y desastroso de mantener. Ese mismo autor aparece embebido en cada uno de sus posts, y el día que cambie su nombre tendrás que perseguir cada copia o convivir con la incoherencia. La normalización es la respuesta que la teoría de bases de datos dio a este problema hace medio siglo, traída al store del cliente: guarda cada entidad una sola vez, identifícala por su clave, y expresa las relaciones como referencias a esa clave, nunca como anidamiento. Tu store deja de ser un árbol de datos duplicados y se convierte en lo que en realidad es: una pequeña base de datos en memoria.
- Diagnosticar las patologías del árbol anidado: duplicación, anomalías de actualización y updates profundos.
- Definir la forma normalizada
idsmásentitiesy su analogía con las tablas relacionales. - Modelar relaciones uno-a-muchos y muchos-a-muchos por id, y explicar por qué eso estabiliza referencias.
- Situar el coste de desnormalizar al leer y las herramientas de 2026 para normalizar.
Las patologías del árbol anidado
Un estado anidado sufre exactamente las mismas enfermedades que una base de datos sin normalizar, y por la misma razón: guarda el mismo hecho en varios sitios. La primera es la duplicación: el autor incrustado en cada post ocupa espacio y, peor, existe en múltiples copias. La segunda es la anomalía de actualización: cambiar el nombre del autor exige encontrar y reescribir todas sus apariciones, y basta olvidar una para que el store se contradiga a sí mismo. La tercera es el update profundo: modificar algo enterrado bajo cinco niveles obliga, con inmutabilidad (Nivel 7), a clonar toda la cadena de padres, un código verboso y frágil. La cuarta es la búsqueda: encontrar una entidad por su id significa recorrer el árbol, cuando debería ser un acceso directo.
Duplicación
El mismo autor vive incrustado en cada post. Una verdad, muchas copias que pueden divergir.
Anomalía de actualización
Cambias el nombre en un sitio y las otras copias quedan rancias. El store se contradice a sí mismo.
Update profundo
Tocar algo a cinco niveles obliga a clonar toda la cadena de padres para respetar la inmutabilidad.
Búsqueda lineal
Hallar una entidad por id exige recorrer el árbol, cuando debería ser un acceso constante.
La forma normalizada: entidades por id
La cura es guardar cada tipo de entidad en su propia colección, con dos partes: un diccionario entities que mapea id a la entidad, y un array ids que fija el orden. Es exactamente una tabla con su clave primaria y su índice.
// Anidado: el autor duplicado dentro de cada post.
const anidado = {
posts: [
{ id: 'p1', texto: 'hola', autor: { id: 'u1', nombre: 'Ada' } },
{ id: 'p2', texto: 'mundo', autor: { id: 'u1', nombre: 'Ada' } },
],
}
// Normalizado: cada entidad una vez, relaciones por id.
const normalizado = {
posts: {
ids: ['p1', 'p2'],
entities: {
p1: { id: 'p1', texto: 'hola', autorId: 'u1' },
p2: { id: 'p2', texto: 'mundo', autorId: 'u1' },
},
},
usuarios: {
ids: ['u1'],
entities: { u1: { id: 'u1', nombre: 'Ada' } }, // una sola verdad
},
}
flowchart TD N[arbol anidado] --> N1[usuario Ada copiado en cada post] N1 --> N2[cambiar el nombre en un sitio deja copias viejas] M[forma normalizada] --> M1[usuario Ada una sola vez en users by id] M1 --> M2[los posts guardan solo autorId] style N2 fill:#f38ba8,color:#11111b style M2 fill:#a6e3a1,color:#11111b
La forma normalizada tipa de maravilla: entities es un Record<string, Entidad> y ids un string[]. El acceso por id pasa de recorrer un array a un simple entities[id], de coste constante.
Modelar las relaciones por id
Normalizar no elimina las relaciones; las expresa como referencias en vez de como anidamiento, igual que las claves foráneas de una base de datos. Una relación uno-a-muchos —un post y sus comentarios— se guarda con la clave del lado uno dentro de cada entidad del lado muchos, o con un array de ids en el lado uno. Una relación muchos-a-muchos —posts y etiquetas— se modela con arrays de ids en ambos lados, o con una colección intermedia, tal como una tabla de unión en el mundo relacional.
// Uno-a-muchos: el comentario apunta a su post por id.
type Comentario = { id: string; texto: string; postId: string }
// Muchos-a-muchos: arrays de ids en ambos lados, nada anidado.
type Post = { id: string; texto: string; etiquetaIds: string[] }
type Etiqueta = { id: string; nombre: string; postIds: string[] }
Guardar solo ids en las relaciones es lo que preserva la identidad: cuando editas una etiqueta, los posts que la referencian no cambian de referencia porque solo guardaban su id, no una copia de ella. Esa es la diferencia mecánica entre un árbol que se clona en cascada y una base de datos donde cada tabla se toca de forma independiente.
Por qué evita bugs
La normalización no es una optimización estética; elimina clases enteras de bugs porque restablece la fuente única de verdad por entidad que vimos en el Nivel 1. Con una sola copia de cada entidad, la incoherencia se vuelve imposible por construcción: no hay copias que puedan divergir. Los updates se vuelven superficiales y locales —tocas entities[id] y nada más—, lo que a su vez preserva referencias: al actualizar un usuario, los posts que no cambiaron conservan su identidad, y por tanto los selectores memoizados y React.memo pueden saltárselos. Esta estabilidad referencial es el puente directo con la lección anterior: la memoización solo rinde si las cosas que no cambian mantienen su referencia, y eso es precisamente lo que un update local en estado normalizado garantiza y un clon profundo del árbol anidado destruye.
La normalización no es gratis: mueve la complejidad del write al read. Guardar es trivial, pero para pintar un post con el nombre de su autor hay que reunir dos colecciones por id —hacer el join—. La buena noticia es que ese trabajo va justo donde debe ir: en un selector, compuesto y memoizado. El join se paga una vez por cambio real de datos, no en cada render, gracias a createSelector. La normalización y la memoización son piezas de un mismo diseño: la primera crea referencias estables, la segunda las aprovecha; separadas, cada una rinde la mitad.
Cómo llegar allí en 2026
Históricamente, la respuesta del servidor se transformaba a la forma normalizada con normalizr, definiendo un esquema de entidades. Sigue siendo válido y útil cuando la forma anidada es compleja e irregular. Pero en el ecosistema de Redux Toolkit de 2026 rara vez normalizas a mano: createEntityAdapter —la próxima lección— te da la forma ids más entities, los reducers para mantenerla y los selectores para leerla; y RTK Query gestiona su propia caché de servidor, sobre la que puedes volcar entidades con el adapter. La decisión ya no es cómo normalizar, sino qué merece ser tratado como entidad de primera clase.
La normalización es la forma correcta para colecciones de entidades con identidad y reutilización, pero aplicarla a todo es su propio error. Un estado efímero de UI —qué pestaña está activa, si un modal está abierto—, una lista corta y fija sin identidad natural, o un objeto de configuración que se lee entero y no se comparte no ganan nada volviéndose ids más entities; ganan ceremonia. La regla es la misma que atraviesa el track: normaliza lo que es de verdad una entidad compartida, y deja plano lo que no lo es. La cuarta y la quinta lección afilan justo este criterio.
La normalización parece un truco de Redux y es, en realidad, la aplicación al cliente de una de las ideas más asentadas de la informática: la que Edgar Codd formuló para las bases de datos relacionales. El principio es idéntico —cada hecho debe estar guardado en un único lugar— y las patologías que cura son las mismas anomalías de actualización, inserción y borrado que motivaron las formas normales hace cincuenta años. Reconocer esto cambia cómo diseñas el estado: dejas de preguntarte cómo llega el dato del servidor y empiezas a preguntarte cuál es tu esquema, cuáles son tus entidades, cuáles sus claves y cuáles sus relaciones, exactamente como diseñarías tablas. El árbol anidado que devuelve una API no es un modelo de datos, es un mensaje de transporte optimizado para viajar por la red en una sola petición; confundir el formato de transporte con el modelo de almacenamiento es el error de origen del que brotan casi todos los bugs de estado compartido. El programador maduro desnormaliza para viajar y normaliza para guardar, y sabe que su store no es un contenedor pasivo de lo que llegó, sino una base de datos en memoria con su esquema deliberado, sus índices y sus consultas —los selectores— por encima. Una vez ves el store con esos ojos, la normalización deja de ser una técnica que aplicas y pasa a ser la forma por defecto en que piensas el estado compartido.
- Toma una respuesta anidada de tu API con una entidad repetida (un autor, una categoría) y dibuja su árbol señalando cada copia duplicada.
- Transfórmala a la forma
idsmásentities, con una colección por tipo de entidad y las relaciones expresadas por id. - Modela una relación muchos-a-muchos de tu dominio con arrays de ids en ambos lados y compárala con cómo la anidarías.
- Escribe el update que cambia el nombre de una entidad y compara: una línea en normalizado frente a perseguir copias en anidado.
- Implementa el selector de join que reúne una entidad con su relación por id, y memoízalo con
createSelector. - Identifica en tu app un dato que NO merezca normalizarse y justifica por qué dejarlo plano es la decisión correcta.