Ir al contenido principal
Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Normalización de la Forma del Estado

Muchas aplicaciones manejan datos que son inherentemente anidados o relacionales. Por ejemplo, un editor de blogs podría tener muchas Publicaciones, cada Publicación podría tener muchos Comentarios, y tanto las Publicaciones como los Comentarios serían escritos por un Usuario. Los datos para este tipo de aplicación podrían verse así:

const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]

Observa que la estructura de los datos es un tanto compleja, y algunos datos están repetidos. Esto es problemático por varias razones:

  • Cuando un dato está duplicado en varios lugares, se vuelve más difícil asegurar que se actualice correctamente.

  • Los datos anidados implican que la lógica del reductor correspondiente debe ser más anidada y, por lo tanto, más compleja. En particular, intentar actualizar un campo profundamente anidado puede volverse engorroso rápidamente.

  • Dado que las actualizaciones inmutables de datos requieren que todos los ancestros en el árbol del estado se copien y actualicen también, y que las nuevas referencias de objetos harán que los componentes de UI conectados se vuelvan a renderizar, una actualización a un objeto de datos profundamente anidado podría forzar a componentes de UI totalmente irrelevantes a volverse a renderizar, incluso si los datos que muestran no han cambiado realmente.

Debido a esto, el enfoque recomendado para gestionar datos relacionales o anidados en un almacén de Redux es tratar una parte de tu almacén como si fuera una base de datos y mantener esos datos en una forma normalizada.

Diseño de un Estado Normalizado

Los conceptos básicos de la normalización de datos son:

  • Cada tipo de dato obtiene su propia "tabla" en el estado.

  • Cada "tabla de datos" debe almacenar los elementos individuales en un objeto, usando los IDs de los elementos como claves y los propios elementos como valores.

  • Cualquier referencia a elementos individuales debe hacerse almacenando el ID del elemento.

  • Se deben usar arreglos de IDs para indicar el orden.

Un ejemplo de estructura de estado normalizado para el ejemplo del blog anterior podría verse así:

{
posts: {
byId: {
post1: {
id: "post1",
author: "user1",
body: "......",
comments: ["comment1", "comment2"]
},
post2: {
id: "post2",
author: "user2",
body: "......",
comments: ["comment3", "comment4", "comment5"]
}
},
allIds: ["post1", "post2"]
},
comments: {
byId: {
comment1: {
id: "comment1",
author: "user2",
comment: "....."
},
comment2: {
id: "comment2",
author: "user3",
comment: "....."
},
comment3: {
id: "comment3",
author: "user3",
comment: "....."
},
comment4: {
id: "comment4",
author: "user1",
comment: "....."
},
comment5: {
id: "comment5",
author: "user3",
comment: "....."
}
},
allIds: ["comment1", "comment2", "comment3", "comment4", "comment5"]
},
users: {
byId: {
user1: {
username: "user1",
name: "User 1"
},
user2: {
username: "user2",
name: "User 2"
},
user3: {
username: "user3",
name: "User 3"
}
},
allIds: ["user1", "user2", "user3"]
}
}

Esta estructura de estado es mucho más plana en general. Comparada con el formato anidado original, representa una mejora en varios aspectos:

  • Como cada elemento está definido en un solo lugar, no tenemos que intentar hacer cambios en múltiples sitios si ese elemento se actualiza.

  • La lógica del reductor no tiene que lidiar con niveles profundos de anidamiento, por lo que probablemente será mucho más simple.

  • La lógica para recuperar o actualizar un elemento dado es ahora bastante simple y consistente. Dado el tipo de elemento y su ID, podemos buscarlo directamente en un par de pasos sencillos, sin tener que hurgar en otros objetos para encontrarlo.

  • Como cada tipo de dato está separado, una actualización como cambiar el texto de un comentario solo requeriría nuevas copias de la porción "comments > byId > comment" del árbol. Esto generalmente significará menos partes de la UI que necesitan actualizarse porque sus datos han cambiado. En contraste, actualizar un comentario en la forma anidada original habría requerido actualizar el objeto comentario, el objeto publicación padre, el arreglo de todos los objetos de publicación, y probablemente habría causado que todos los componentes de Publicación y Comentario en la UI se volvieran a renderizar.

Ten en cuenta que una estructura de estado normalizada generalmente implica que más componentes estén conectados y que cada componente sea responsable de buscar sus propios datos, a diferencia de unos pocos componentes conectados que buscan grandes cantidades de datos y pasan todos esos datos hacia abajo. Resulta que tener componentes padres conectados que simplemente pasan IDs de elementos a hijos conectados es un buen patrón para optimizar el rendimiento de la UI en una aplicación React Redux, por lo que mantener el estado normalizado juega un papel clave en la mejora del rendimiento.

Organización de Datos Normalizados en el Estado

Una aplicación típica probablemente tendrá una mezcla de datos relacionales y no relacionales. Si bien no hay una regla única sobre cómo deben organizarse exactamente estos diferentes tipos de datos, un patrón común es colocar las "tablas" relacionales bajo una clave principal común, como "entities". Una estructura de estado que usa este enfoque podría verse así:

{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities: {
entityType1 : {....},
entityType2 : {....}
},
ui: {
uiSection1 : {....},
uiSection2 : {....}
}
}

Esto podría ampliarse de varias formas. Por ejemplo, una aplicación que realiza muchas ediciones de entidades podría mantener dos conjuntos de "tablas" en el estado: una para los valores "actuales" de los elementos y otra para los valores "en progreso". Al editar un elemento, sus valores podrían copiarse a la sección "en progreso", y cualquier acción de actualización se aplicaría a esta copia "en progreso", permitiendo que el formulario de edición se controle mediante esos datos mientras otras partes de la UI siguen referenciando la versión original. "Restablecer" el formulario implicaría eliminar el elemento de "en progreso" y volver a copiar los datos originales de "actual" a "en progreso", mientras que "aplicar" los cambios conllevaría copiar los valores de "en progreso" a "actual".

Relaciones y tablas

Al tratar parte de nuestro almacén Redux como una "base de datos", muchos principios de diseño de bases de datos también aplican aquí. Por ejemplo, para relaciones muchos-a-muchos podemos usar una tabla intermedia que almacene IDs de elementos relacionados (conocida como "tabla de unión" o "tabla asociativa"). Para mantener consistencia, convendría usar el mismo enfoque byId y allIds que en las tablas principales, así:

{
entities: {
authors: {
byId: {},
allIds: []
},
books: {
byId: {},
allIds: []
},
authorBook: {
byId: {
1: {
id: 1,
authorId: 5,
bookId: 22
},
2: {
id: 2,
authorId: 5,
bookId: 15
},
3: {
id: 3,
authorId: 42,
bookId: 12
}
},
allIds: [1, 2, 3]
}
}
}

Operaciones como "Buscar todos los libros de este autor" se resuelven fácilmente con un simple bucle sobre la tabla de unión. Dado el volumen típico de datos en aplicaciones cliente y la velocidad de los motores JavaScript, este enfoque suele ofrecer rendimiento suficiente para la mayoría de casos.

Normalización de datos anidados

Como las APIs suelen devolver datos anidados, estos deben transformarse a una forma normalizada antes de incluirlos en el árbol de estado. Normalmente se usa la biblioteca Normalizr para esta tarea: defines tipos de esquema y relaciones, proporcionas el esquema y los datos de respuesta a Normalizr, y devuelve una transformación normalizada. Esta salida puede incluirse en una acción y usarse para actualizar el almacén. Consulta la documentación de Normalizr para más detalles.