Estado como matriz de objetos vs objeto codificado por id


94

En el capítulo sobre Diseño de la forma del estado , los documentos sugieren mantener su estado en un objeto codificado por ID:

Mantenga todas las entidades de un objeto almacenadas con un ID como clave y utilice ID para hacer referencia a ellas desde otras entidades o listas.

Continúan diciendo

Piense en el estado de la aplicación como una base de datos.

Estoy trabajando en la forma del estado para obtener una lista de filtros, algunos de los cuales estarán abiertos (se muestran en una ventana emergente) o tienen opciones seleccionadas. Cuando leí "Piense en el estado de la aplicación como una base de datos", pensé en pensar en ellos como una respuesta JSON, ya que se devolvería 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 a

{
   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 siempre que los datos sean serializables (bajo el título "Estado") .

Así que fui felizmente con el enfoque de matriz de objetos, hasta que escribí mi reductor.

Con el enfoque de objeto codificado por id (y el uso liberal de la sintaxis de propagación), la OPEN_FILTERparte 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 optar por el enfoque de objeto codificado por id? ¿Hay otras ventajas en esa forma de estado?

y

2) Parece que el enfoque de objeto con clave por id hace que sea más difícil lidiar con la entrada / salida JSON estándar para una API. (Es por eso que me decidí por la matriz de objetos en primer lugar). Entonces, si sigue ese enfoque, ¿usa una función para transformarlo 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 eso 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 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 "recomendado" mantenerlo como un objeto codificado por ID, o hay otros puntos de dolor imprevistos con los que me voy a encontrar al usar una matriz de objetos que hacen que deba abortar eso? ¿Planea e intenta ceñirse a un objeto codificado por ID?


2
Esta es una pregunta interesante, y también una que tenía, solo para proporcionar una idea, aunque tiendo a normalizar en redux en lugar de matrices (simplemente porque buscar es más fácil), encuentro que si toma el enfoque normalizado, la clasificación se vuelve un problema porque no obtiene la misma estructura que le da la matriz, por lo que se ve obligado a ordenar usted mismo.
Robert Saunders

Veo un problema en el enfoque 'object-keyed-by-id', sin embargo, esto no es frecuente, pero debemos considerar este caso al escribir cualquier aplicación de interfaz de usuario. Entonces, ¿qué pasa si quiero cambiar el orden de la entidad usando un elemento de arrastrar y soltar listado como lista ordenada? Por lo general, el enfoque 'object-keyed-by-id' falla aquí y seguramente optaría por una variedad de enfoques de objetos para evitar problemas tan generosos. Podría haber más, pero pensé en compartir esto aquí
Kunal Navhate

¿Cómo se puede clasificar un objeto hecho de objetos? Esto parece imposible.
David Vielhuber

@DavidVielhuber ¿Quieres decir además de usar algo como lodash's sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme

Si. Actualmente convertimos esos objetos en matrices dentro de una propiedad calculada de vue
David Vielhuber

Respuestas:


46

P1: La simplicidad del reductor es el resultado de no tener que buscar en la matriz para encontrar la entrada correcta. La ventaja es no tener que buscar en la matriz. Los selectores y otros proveedores de acceso a datos pueden acceder, ya menudo lo hacen, a estos elementos mediante id. Tener que buscar en la matriz para cada acceso se convierte en un problema de rendimiento. Cuando sus matrices aumentan de tamaño, el problema de rendimiento empeora abruptamente. Además, a medida que su aplicación se vuelve más compleja y muestra y filtra datos en más lugares, el problema también empeora. La combinación puede resultar perjudicial. Al acceder a los elementos por id, el tiempo de acceso cambia de O(n)a O(1), lo que para los elementos grandes n(en este caso, los elementos de matriz) marca una gran diferencia.

P2: Puede usarlo normalizrpara ayudarlo con la conversión de API a tienda. A partir de normalizr V3.1.0 puede usar desnormalize para ir al revés. Dicho esto, las aplicaciones suelen ser más consumidores que productores de datos y, como tal, la conversión a la tienda suele realizarse 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.


El normalizador es de nuevo lo que seguramente crearía dolor una vez que cambiemos las definiciones en el backend. Así que esto tiene que mantenerse actualizado todo el tiempo
Kunal Navhate

12

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 utilizar siempre ese ID 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, debe pasar todo el objeto y eso puede complicarse muy pronto, puede terminar recreando el objeto en diferentes reductores, acciones o incluso en el contenedor (no quiere eso). Las vistas siempre podrán obtener el objeto completo incluso si su reductor asociado solo contiene la ID, porque al mapear el estado, obtendrá la colección en alguna parte (la vista obtiene el estado completo para asignarlo a las propiedades). Por todo lo que he dicho, las acciones terminan teniendo la mínima cantidad de parámetros y reducen la mínima cantidad de información, pruébalo,

2) La conexión a la API no debe afectar la arquitectura de tu almacenamiento y reductores, por eso tienes acciones, para mantener la separación de preocupaciones. Simplemente coloque 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 todo.

3) Usé matrices para estructuras con ID, y estas son las consecuencias imprevistas que he sufrido:

  • Recreando objetos constantemente a lo largo del código
  • Pasar 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. Se le ha advertido, no se meta en problemas.

También:

4) La mayoría de las colecciones con ID están destinadas a usar el ID como referencia al objeto completo, debería aprovechar eso. Las llamadas a la API obtendrán el ID y luego el resto de los parámetros, al igual que sus acciones y reductores.


Me estoy encontrando con un problema en el que tenemos una aplicación con una gran cantidad de datos (de 1000 a 10,000) almacenados por identificación en un objeto en la tienda redux. En las vistas, todos usan matrices ordenadas para mostrar datos de series de tiempo. Esto significa que cada vez que se realiza una reproducción, debe tomar todo el objeto, convertirlo en una matriz y ordenarlo. Se me asignó la tarea de mejorar el rendimiento de la aplicación. ¿Es este un caso de uso en el que tiene más sentido almacenar sus datos en una matriz ordenada y usar la búsqueda binaria para eliminar y actualizar en lugar de un objeto?
William Chou

Terminé teniendo que hacer otros mapas hash derivados de estos datos para minimizar el tiempo de cálculo en las actualizaciones. Esto hace que la actualización de todas las diferentes vistas requiera su propia lógica de actualización. Antes de esto, todos los componentes tomarían el objeto de la tienda y reconstruirían las estructuras de datos que necesitaban para hacer su vista. La única forma en la que puedo pensar para garantizar un jankiness mínimo en la interfaz de usuario es usar un trabajador web para hacer la conversión de objeto a matriz. La compensación de esto es una recuperación y una lógica de actualización más sencillas, ya que todos los componentes solo dependen de un tipo de datos para leer y escribir.
William Chou

8

1) ¿Es la simplicidad del reductor la motivación para optar por el enfoque de objeto codificado por id? ¿Hay otras ventajas en esa forma de estado?

La razón principal por la que desea mantener las entidades en objetos almacenados con ID como claves (también llamado normalizado ) 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 digamos que desea crear un componente que muestre una lista de todos los usuarios que han creado opciones. Para hacer eso, primero tendría que solicitar todos los elementos, luego iterar sobre cada una de sus opciones y, por último, obtener el 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 eficiente listar todos los usuarios que han creado opciones (los tenemos aislados en entity.optionCreators, así que solo tenemos que recorrer esa lista).

También es bastante sencillo 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 objeto con clave por id hace que sea más difícil lidiar con la entrada / salida JSON estándar para una API. (Es por eso que me decidí por la matriz de objetos en primer lugar). Entonces, si sigue ese enfoque, ¿usa una función para transformarlo 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 eso es menos torpe que el reductor de matriz de objetos anterior?)

Una respuesta JSON se puede normalizar usando, por ejemplo, normalizr .

3) Sé que Dan Abramov diseñó redux para ser teóricamente agnóstico de estructura de datos de estado (como 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 "recomendado" mantenerlo como un objeto codificado por ID, o hay otros puntos de dolor imprevistos con los que me voy a encontrar al usar una matriz de objetos que hacen que deba abortar eso? ¿Planea e intenta quedarse con un objeto codificado por ID?

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 mucho.


1
mapdevuelve indefinido como aquí , si los recursos se obtienen por separado, lo que hace que filters sea demasiado complicado. ¿Existe una solución?
Saravanabalagi Ramachandran

1
@tobiasandersen, ¿cree que está bien que el servidor devuelva datos normalizados ideales para react / redux, para evitar que el cliente haga la conversión a través de libs como normalizr? En otras palabras, haga que el servidor normalice los datos y no el cliente.
Mateo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.