javascript - actions - redux thunk
Estado como conjunto de objetos frente a objeto con clave por id (3)
En el capítulo sobre Diseño de la forma del estado , los documentos sugieren mantener su estado en un objeto con clave por ID:
Mantenga cada entidad en un objeto almacenado con una ID como clave, y use ID para hacer referencia a ella desde otras entidades o listas.
Pasan a declarar
Piense en el estado de la aplicación como una base de datos.
Estoy trabajando en la forma de estado para una lista de filtros, algunos de los cuales estarán abiertos (se muestran en una ventana emergente) o tienen opciones seleccionadas. Cuando leí "Piensa en el estado de la aplicación como una base de datos", pensé en pensar en ellos como una respuesta JSON, ya que sería devuelta desde una API (respaldada por una base de datos).
Así que estaba pensando en ello como
[{
id: ''1'',
name: ''View'',
open: false,
options: [''10'', ''11'', ''12'', ''13''],
selectedOption: [''10''],
parent: null,
},
{
id: ''10'',
name: ''Time & Fees'',
open: false,
options: [''20'', ''21'', ''22'', ''23'', ''24''],
selectedOption: null,
parent: ''1'',
}]
Sin embargo, los documentos sugieren un formato más parecido
{
1: {
name: ''View'',
open: false,
options: [''10'', ''11'', ''12'', ''13''],
selectedOption: [''10''],
parent: null,
},
10: {
name: ''Time & Fees'',
open: false,
options: [''20'', ''21'', ''22'', ''23'', ''24''],
selectedOption: null,
parent: ''1'',
}
}
En teoría, no debería importar mientras los datos sean serializables (bajo el encabezado "Estado") .
Así que seguí felizmente el enfoque de la matriz de objetos, hasta que escribí mi reductor.
Con el enfoque de identificación por objeto (y el uso liberal de la sintaxis de propagación), la parte
OPEN_FILTER
del reductor se convierte en
switch (action.type) {
case OPEN_FILTER: {
return { ...state, { ...state[action.id], open: true } }
}
Mientras que con el enfoque de matriz de objetos, es más detallado (y dependiente de la función auxiliar)
switch (action.type) {
case OPEN_FILTER: {
// relies on getFilterById helper function
const filter = getFilterById(state, action.id);
const index = state.indexOf(filter);
return state
.slice(0, index)
.concat([{ ...filter, open: true }])
.concat(state.slice(index + 1));
}
...
Entonces mis preguntas son triples:
1) ¿Es la simplicidad del reductor la motivación para seguir el enfoque de identificación por objeto? ¿Hay otras ventajas en la forma de ese estado?
y
2) Parece que el enfoque de identificación de objetos por clave hace que sea más difícil lidiar con la entrada / salida JSON estándar para una API. (Es por eso que elegí la matriz de objetos en primer lugar). Entonces, si sigues ese enfoque, ¿solo usas una función para transformarlo de un lado a otro entre el formato JSON y el formato de forma de estado? Eso parece torpe. (Aunque si defiende ese enfoque, ¿es parte de su razonamiento que es menos torpe que el reductor de matriz de objetos anterior?)
y
3) Sé que Dan Abramov diseñó redux para ser teóricamente agnóstico de estructura de datos de estado (como lo sugiere "Por convención, el estado de nivel superior es un objeto o alguna otra colección de valores clave como un Mapa, pero técnicamente puede ser cualquier tipo , " énfasis mío). Pero dado lo anterior, ¿es simplemente "recomendable" mantenerlo como un objeto con clave de ID, o hay otros puntos de dolor imprevistos con los que me voy a encontrar usando una serie de objetos que hacen que deba abortar eso? planificar e intentar seguir con un objeto con clave de identificación?
1) ¿Es la simplicidad del reductor la motivación para seguir el enfoque de identificación por objeto? ¿Hay otras ventajas en la forma de ese estado?
La razón principal por la que desea mantener las entidades en objetos almacenados con ID como claves (también llamadas normalizadas ), es que es realmente engorroso trabajar con objetos profundamente anidados (que es lo que normalmente obtiene de las API REST en una aplicación más compleja): tanto para sus componentes como para sus reductores.
Es un poco difícil ilustrar los beneficios de un estado normalizado con su ejemplo actual (ya que no tiene una estructura profundamente anidada ). Pero digamos que las opciones (en su ejemplo) también tenían un título, y fueron creadas por usuarios en su sistema. Eso haría que la respuesta se viera así:
[{
id: 1,
name: ''View'',
open: false,
options: [
{
id: 10,
title: ''Option 10'',
created_by: {
id: 1,
username: ''thierry''
}
},
{
id: 11,
title: ''Option 11'',
created_by: {
id: 2,
username: ''dennis''
}
},
...
],
selectedOption: [''10''],
parent: null,
},
...
]
Ahora supongamos que desea crear un componente que muestre una lista de todos los usuarios que han creado opciones. Para hacer eso, primero debe solicitar todos los elementos, luego iterar sobre cada una de sus opciones y, por último, obtener created_by.username.
Una mejor solución sería normalizar la respuesta en:
results: [1],
entities: {
filterItems: {
1: {
id: 1,
name: ''View'',
open: false,
options: [10, 11],
selectedOption: [10],
parent: null
}
},
options: {
10: {
id: 10,
title: ''Option 10'',
created_by: 1
},
11: {
id: 11,
title: ''Option 11'',
created_by: 2
}
},
optionCreators: {
1: {
id: 1,
username: ''thierry'',
},
2: {
id: 2,
username: ''dennis''
}
}
}
Con esta estructura, es mucho más fácil y más eficiente enumerar todos los usuarios que han creado opciones (las tenemos aisladas en entidades.optionCreators, por lo que solo tenemos que recorrer esa lista).
También es bastante simple mostrar, por ejemplo, los nombres de usuario de aquellos que han creado opciones para el elemento de filtro con ID 1:
entities
.filterItems[1].options
.map(id => entities.options[id])
.map(option => entities.optionCreators[option.created_by].username)
2) Parece que el enfoque de identificación de objetos por clave hace que sea más difícil lidiar con la entrada / salida JSON estándar para una API. (Es por eso que elegí la matriz de objetos en primer lugar). Entonces, si sigues ese enfoque, ¿solo usas una función para transformarlo de un lado a otro entre el formato JSON y el formato de forma de estado? Eso parece torpe. (Aunque si defiende ese enfoque, ¿es parte de su razonamiento que es menos torpe que el reductor de matriz de objetos anterior?)
Una respuesta JSON puede normalizarse usando, por ejemplo, normalizr .
3) Sé que Dan Abramov diseñó redux para ser teóricamente agnóstico de estructura de datos de estado (como lo sugiere "Por convención, el estado de nivel superior es un objeto o alguna otra colección de valores clave como un Mapa, pero técnicamente puede ser cualquier tipo, "énfasis mío). Pero dado lo anterior, ¿es simplemente "recomendable" mantenerlo como un objeto con clave de ID, o hay otros puntos de dolor imprevistos con los que me voy a encontrar usando una serie de objetos que hacen que deba abortar eso? planificar e intentar seguir con un objeto con clave de identificación?
Probablemente sea una recomendación para aplicaciones más complejas con muchas respuestas API profundamente anidadas. Sin embargo, en su ejemplo particular, realmente no importa tanto.
Piense en el estado de la aplicación como una base de datos.
Esa es la idea clave.
1) Tener objetos con ID únicos le permite usar siempre esa identificación al hacer referencia al objeto, por lo que debe pasar la cantidad mínima de datos entre acciones y reductores. Es más eficiente que usar array.find (...). Si usa el enfoque de matriz, tiene que pasar todo el objeto y eso puede volverse desordenado muy pronto, puede terminar recreando el objeto en diferentes reductores, acciones o incluso en el contenedor (no lo desea). Las vistas siempre podrán obtener el objeto completo, incluso si su reductor asociado solo contiene la ID, porque al asignar el estado obtendrá la colección en algún lugar (la vista obtiene todo el estado para asignarla a las propiedades). Debido a todo lo que he dicho, las acciones terminan teniendo la cantidad mínima de parámetros y reducen la cantidad mínima de información, pruébelo, pruebe ambos métodos y verá que la arquitectura termina siendo más escalable y limpia usando ID si las colecciones tienen ID.
2) La conexión a la API no debe afectar la arquitectura de su almacenamiento y reductores, es por eso que tiene acciones, para mantener la separación de las preocupaciones. Simplemente ponga su lógica de conversión dentro y fuera de la API en un módulo reutilizable, importe ese módulo en las acciones que usan la API, y eso debería ser.
3) Utilicé matrices para estructuras con ID, y estas son las consecuencias imprevistas que he sufrido:
- Recreando objetos constantemente por el código
- Transmitir información innecesaria a reductores y acciones
- Como consecuencia de eso, código malo, no limpio y no escalable.
Terminé cambiando mi estructura de datos y reescribiendo mucho código. Te han advertido, por favor no te metas en problemas.
También:
4) La mayoría de las colecciones con ID están destinadas a usar la ID como referencia para todo el objeto, debe aprovechar eso. Las llamadas a la API obtendrán la ID y luego el resto de los parámetros, al igual que sus acciones y reductores.
P1: La simplicidad del reductor es el resultado de no tener que buscar en la matriz para encontrar la entrada correcta.
No tener que buscar a través de la matriz es la ventaja.
Los selectores y otros accesores de datos pueden y a menudo acceden a estos elementos por
id
.
Tener que buscar a través de la matriz para cada acceso se convierte en un problema de rendimiento.
Cuando sus matrices se hacen más grandes, el problema de rendimiento empeora abruptamente.
Además, a medida que su aplicación se vuelve más compleja, mostrando y filtrando datos en más lugares, el problema también empeora.
La combinación puede ser perjudicial.
Al acceder a los elementos por
id
, el tiempo de acceso cambia de
O(n)
a
O(1)
, lo que para
n
grande (aquí elementos de matriz) hace una gran diferencia.
P2: Puede usar
normalizr
para ayudarlo con la conversión de API a tienda.
A partir de normalizr V3.1.0, puede usar desnormalizar para ir hacia otro lado.
Dicho esto, las aplicaciones a menudo son más consumidores que productores de datos y, como tal, la conversión a la tienda generalmente se realiza con más frecuencia.
P3: Los problemas con los que se encontrará al usar una matriz no son tanto problemas con la convención de almacenamiento y / o incompatibilidades, sino más problemas de rendimiento.