Estado como matriz de objetos vs objeto codificado por id

94

No capítulo sobre como projetar a forma do estado , os documentos sugerem manter seu estado em um objeto codificado por ID:

Mantenha cada entidade em um objeto armazenado com um ID como uma chave e use IDs para referenciá-lo a partir de outras entidades ou listas.

Eles continuam a afirmar

Pense no estado do aplicativo como um banco de dados.

Estou trabalhando na forma de estado para uma lista de filtros, alguns dos quais serão abertos (são exibidos em um pop-up) ou têm opções selecionadas. Quando li "Pense no estado do aplicativo como um banco de dados", pensei em pensar neles como uma resposta JSON, pois seria retornada de uma API (ela própria apoiada por um banco de dados).

Então eu estava pensando nisso 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',
  }]

No entanto, os documentos sugerem um formato mais parecido com

{
   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',
  }
}

Em teoria, isso não deve importar, desde que os dados sejam serializáveis ​​(sob o título "Estado") .

Então, fui feliz com a abordagem de matriz de objetos, até que estava escrevendo meu redutor.

Com a abordagem object-keyed-by-id (e uso liberal da sintaxe de propagação), a OPEN_FILTERparte do redutor torna-se

switch (action.type) {
  case OPEN_FILTER: {
    return { ...state, { ...state[action.id], open: true } }
  }

Considerando que, com a abordagem de matriz de objetos, é mais prolixo (e dependente da função 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));
    }
    ...

Portanto, minhas perguntas são três:

1) A simplicidade do redutor é a motivação para seguir a abordagem de objeto-chave-por-id? Existem outras vantagens nessa forma de estado?

e

2) Parece que a abordagem objeto-chave-por-id torna mais difícil lidar com entrada / saída JSON padrão para uma API. (É por isso que optei pela matriz de objetos em primeiro lugar.) Então, se você seguir essa abordagem, você apenas usa uma função para transformá-la entre o formato JSON e o formato de estado? Isso parece desajeitado. (Embora, se você defender essa abordagem, parte do seu raciocínio seja menos desajeitado do que o redutor de matriz de objetos acima?)

e

3) Eu sei que Dan Abramov projetou redux para ser teoricamente agnóstico de estrutura de dados de estado (como sugerido por "Por convenção, o estado de nível superior é um objeto ou alguma outra coleção de valor-chave como um Mapa, mas tecnicamente pode ser qualquer tipo , " ênfase minha). Mas, dado o exposto acima, é apenas "recomendado" mantê-lo como um objeto codificado por ID, ou há outros pontos de dor imprevistos que vou encontrar usando uma matriz de objetos que o tornam tal que eu deveria simplesmente abortar isso planeja e tenta ficar com um objeto codificado por ID?

nickcoxdotme
fonte
2
Esta é uma pergunta interessante, e uma que eu também tinha, apenas para fornecer algumas dicas, embora eu tenha a tendência de normalizar em redux em vez de arrays (puramente porque pesquisar é mais fácil), eu acho que se você seguir a abordagem normalizada, a classificação se torna um problema porque você não obtém a mesma estrutura que o array oferece, então você é forçado a se classificar.
Robert Saunders
Eu vejo um problema na abordagem 'objeto-chave-por-id', no entanto, isso não é frequente, mas temos que considerar este caso ao escrever qualquer aplicativo de IU. E se eu quiser alterar a ordem da entidade usando um elemento arrastar e soltar listado como lista ordenada? Normalmente, a abordagem "objeto-chave-por-id" falha aqui e eu certamente escolheria a abordagem de matriz de objeto para evitar questões tão generosas. Poderia haver mais, mas pensei em compartilhar isso aqui
Kunal Navhate
Como você pode classificar um objeto feito de objetos? Isso parece impossível.
David Vielhuber
@DavidVielhuber Você quer dizer além de usar algo como o Lodash sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme
Sim. Atualmente, convertemos esses objetos em matrizes dentro de uma propriedade computada vue
David Vielhuber

Respostas:

46

Q1: A simplicidade do redutor é o resultado de não ter que pesquisar na matriz para encontrar a entrada correta. Não ter que pesquisar na matriz é a vantagem. Os seletores e outros acessadores de dados podem acessar esses itens e geralmente acessam id. Ter que pesquisar na matriz para cada acesso se torna um problema de desempenho. Quando seus arrays ficam maiores, o problema de desempenho piora drasticamente. Além disso, à medida que seu aplicativo se torna mais complexo, mostrando e filtrando dados em mais lugares, o problema também piora. A combinação pode ser prejudicial. Acessando os itens por id, o tempo de acesso muda de O(n)para O(1), o que para grandes n(aqui itens de array) faz uma grande diferença.

P2: Você pode usar normalizrpara ajudá-lo com a conversão de API para loja. A partir do normalizr V3.1.0, você pode usar desnormalizar para fazer o contrário. Dito isso, os aplicativos costumam ser mais consumidores do que produtores de dados e, como tal, a conversão para armazenamento geralmente é feita com mais frequência.

P3: Os problemas que você encontrará ao usar um array não são tanto problemas com a convenção de armazenamento e / ou incompatibilidades, mas mais problemas de desempenho.

DDS
fonte
normalizador é novamente o que certamente criaria dor, uma vez que alteramos os defs no backend. Portanto, isso deve ser sempre atualizado
Kunal Navhate
12

Pense no estado do aplicativo como um banco de dados.

Essa é a ideia principal.

1) Ter objetos com IDs únicos permite que você sempre use aquele id ao fazer referência ao objeto, então você tem que passar a quantidade mínima de dados entre ações e redutores. É mais eficiente do que usar array.find (...). Se você usar a abordagem de array, terá que passar o objeto inteiro e isso pode ficar confuso muito em breve, você pode acabar recriando o objeto em diferentes redutores, ações ou mesmo no contêiner (você não quer isso). As visualizações sempre serão capazes de obter o objeto completo, mesmo que seu redutor associado contenha apenas o ID, porque ao mapear o estado você obterá a coleção em algum lugar (a visualização obtém todo o estado para mapeá-lo para as propriedades). Por tudo o que eu disse, as ações acabam tendo a quantidade mínima de parâmetros e reduzem a quantidade mínima de informações, experimente,

2) A conexão com a API não deve afetar a arquitetura de seu storage e redutores, por isso você tem ações, para manter a separação de preocupações. Basta colocar sua lógica de conversão dentro e fora da API em um módulo reutilizável, importar esse módulo nas ações que usam a API, e pronto.

3) Usei arrays para estruturas com IDs, e essas são as consequências imprevistas que sofri:

  • Recriando objetos constantemente em todo o código
  • Passando informações desnecessárias para redutores e ações
  • Como consequência disso, código ruim, não limpo e não escalonável.

Acabei mudando minha estrutura de dados e reescrevendo muito código. Você foi avisado, por favor, não se meta em problemas.

Além disso:

4) A maioria das coleções com IDs são feitas para usar o ID como uma referência para todo o objeto, você deve tirar vantagem disso. As chamadas de API obterão o ID e o restante dos parâmetros, assim como suas ações e redutores.

Marco Scabbiolo
fonte
Estou encontrando um problema em que temos um aplicativo com muitos dados (1000 a 10.000) armazenados por id em um objeto na loja redux. Nas visualizações, todos eles usam matrizes classificadas para exibir dados de série temporal. Isso significa que sempre que um rerender é feito, ele deve pegar o objeto inteiro, convertê-lo em um array e classificá-lo. Fui encarregado de melhorar o desempenho do aplicativo. Este é um caso de uso em que faz mais sentido armazenar seus dados em uma matriz classificada e usar a pesquisa binária para fazer exclusões e atualizações em vez de um objeto?
William Chou
Acabei tendo que fazer alguns outros mapas hash derivados desses dados para minimizar o tempo de computação nas atualizações. Isso faz com que a atualização de todas as visualizações diferentes exija sua própria lógica de atualização. Antes disso, todos os componentes pegariam o objeto do armazenamento e reconstruiriam as estruturas de dados necessárias para fazer sua visualização. A única maneira que consigo pensar para garantir o mínimo de irritabilidade na interface do usuário é usando um web worker para fazer a conversão de objeto em array. A compensação para isso é a lógica de recuperação e atualização mais simples, uma vez que todos os componentes dependem apenas de um tipo de dados para leitura e gravação.
William Chou
8

1) A simplicidade do redutor é a motivação para seguir a abordagem de objeto-chave-por-id? Existem outras vantagens nessa forma de estado?

O principal motivo pelo qual você deseja manter entidades em objetos armazenados com IDs como chaves (também chamados de normalizados ), é que é realmente complicado trabalhar com objetos profundamente aninhados (que é o que você normalmente obtém de APIs REST em um aplicativo mais complexo) - tanto para seus componentes quanto para seus redutores.

É um pouco difícil ilustrar os benefícios de um estado normalizado com seu exemplo atual (já que você não tem uma estrutura profundamente aninhada ). Mas digamos que as opções (no seu exemplo) também tenham um título e foram criadas por usuários em seu sistema. Isso faria com que a resposta fosse mais ou menos assim:

[{
  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,
},
...
]

Agora, digamos que você queira criar um componente que mostra uma lista de todos os usuários que criaram opções. Para fazer isso, primeiro você teria que solicitar todos os itens, em seguida, iterar sobre cada uma de suas opções e, por último, obter o created_by.username.

Uma solução melhor seria normalizar a resposta em:

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'
    }
  }
}

Com essa estrutura, é muito mais fácil e mais eficiente listar todos os usuários que criaram opções (nós os temos isolados em entity.optionCreators, então só temos que percorrer essa lista).

Também é bastante simples mostrar, por exemplo, os nomes de usuário daqueles que criaram opções para o item de filtro com ID 1:

entities
  .filterItems[1].options
  .map(id => entities.options[id])
  .map(option => entities.optionCreators[option.created_by].username)

2) Parece que a abordagem objeto-chave-por-id torna mais difícil lidar com entrada / saída JSON padrão para uma API. (É por isso que optei pela matriz de objetos em primeiro lugar.) Então, se você seguir essa abordagem, você apenas usa uma função para transformá-la entre o formato JSON e o formato de estado? Isso parece desajeitado. (Embora, se você defender essa abordagem, parte do seu raciocínio seja menos desajeitado do que o redutor de matriz de objetos acima?)

Uma resposta JSON pode ser normalizada usando, por exemplo, normalizr .

3) Eu sei que Dan Abramov projetou redux para ser teoricamente agnóstico de estrutura de dados de estado (como sugerido por "Por convenção, o estado de nível superior é um objeto ou alguma outra coleção de valor-chave como um Mapa, mas tecnicamente pode ser qualquer tipo, "ênfase minha). Mas, dado o exposto acima, é apenas "recomendado" mantê-lo como um objeto codificado por ID, ou há outros pontos de dor imprevistos que vou encontrar usando uma matriz de objetos que o tornam tal que eu deveria simplesmente abortar isso planeja e tenta ficar com um objeto codificado por ID?

Provavelmente é uma recomendação para aplicativos mais complexos com muitas respostas de API profundamente aninhadas. Porém, em seu exemplo particular, isso realmente não importa muito.

Tobiasandersen
fonte
1
mapretorna indefinido como aqui , se os recursos forem buscados separadamente, tornando filtero processo muito complicado. Há uma solução?
Saravanabalagi Ramachandran
1
@tobiasandersen você acha que pode o servidor retornar dados normalizados ideais para react / redux, para evitar que o cliente faça a conversão via libs como o normalizr? Ou seja, faça com que o servidor normalize os dados e não o cliente.
Mateus,