Stato come array di oggetti vs oggetto con chiave da id


94

Nel capitolo sulla progettazione della forma dello stato , i documenti suggeriscono di mantenere il proprio stato in un oggetto con chiave ID:

Mantieni ogni entità in un oggetto archiviata con un ID come chiave e usa gli ID per fare riferimento ad essa da altre entità o elenchi.

Continuano a dichiarare

Pensa allo stato dell'app come a un database.

Sto lavorando alla forma dello stato per un elenco di filtri, alcuni dei quali saranno aperti (vengono visualizzati in un popup) o avranno opzioni selezionate. Quando ho letto "Pensa allo stato dell'app come a un database", ho pensato di considerarli come una risposta JSON in quanto sarebbe stata restituita da un'API (a sua volta supportata da un database).

Quindi ci stavo pensando come

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

Tuttavia, i documenti suggeriscono un formato più simile

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

In teoria, non dovrebbe avere importanza fintanto che i dati sono serializzabili (sotto il titolo "Stato") .

Quindi sono andato con l'approccio della matrice di oggetti felicemente, fino a quando non stavo scrivendo il mio riduttore.

Con l'approccio object-keyed-by-id (e l'uso liberale della sintassi diffusa), la OPEN_FILTERparte del riduttore diventa

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

Considerando che con l'approccio array di oggetti, è il più dettagliato (e dipendente dalla funzione di supporto)

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));
    }
    ...

Quindi le mie domande sono triplici:

1) La semplicità del riduttore è la motivazione per seguire l'approccio object-keyed-by-id? Ci sono altri vantaggi in quella forma di stato?

e

2) Sembra che l'approccio object-keyed-by-id renda più difficile gestire l'ingresso / uscita JSON standard per un'API. (Ecco perché ho scelto l'array di oggetti in primo luogo.) Quindi, se segui questo approccio, usi solo una funzione per trasformarlo avanti e indietro tra il formato JSON e il formato della forma dello stato? Sembra goffo. (Anche se sostenete questo approccio, fa parte del vostro ragionamento che è meno goffo del riduttore di array di oggetti sopra?)

e

3) So che Dan Abramov ha progettato il redux per essere teoricamente indipendente dalla struttura dei dati di stato (come suggerito da "Per convenzione, lo stato di primo livello è un oggetto o qualche altra raccolta di valori-chiave come una mappa, ma tecnicamente può essere qualsiasi tipo ", enfasi mia). Ma dato quanto sopra, è semplicemente "consigliato" mantenerlo un oggetto codificato da ID, o ci sono altri punti deboli imprevisti in cui mi imbatterò usando una serie di oggetti che lo rendono tale che dovrei semplicemente interromperlo pianificare e provare a restare con un oggetto con chiave ID?


2
Questa è una domanda interessante, e anche una che ho avuto, solo per fornire alcune informazioni, anche se tendo a normalizzare in redux invece che in array (semplicemente perché cercare è più facile), trovo che se prendi l'approccio normalizzato l'ordinamento diventa un problema perché non ottieni la stessa struttura fornita dall'array, quindi sei costretto a ordinare te stesso.
Robert Saunders

Vedo un problema nell'approccio 'object-keyed-by-id', tuttavia questo non è frequente ma dobbiamo considerare questo caso durante la scrittura di qualsiasi applicazione dell'interfaccia utente. Quindi cosa succede se voglio modificare l'ordine dell'entità utilizzando un elemento di trascinamento elencato come elenco ordinato? Di solito l'approccio "object-keyed-by-id" fallisce qui e io andrei sicuramente con un approccio a matrice di oggetti per evitare problemi così generosi. Potrebbe esserci di più, ma
ho

Come puoi ordinare un oggetto composto da oggetti? Sembra impossibile.
David Vielhuber,

@DavidVielhuber Intendi oltre a usare qualcosa come lodash's sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme

Sì. Attualmente convertiamo quegli oggetti in array all'interno di una proprietà calcolata di vue
David Vielhuber,

Risposte:


46

D1: La semplicità del riduttore è il risultato di non dover cercare nell'array per trovare la voce giusta. Non dover cercare nell'array è il vantaggio. I selettori e altri strumenti di accesso ai dati possono e spesso accedono a questi elementi tramite id. Dover cercare nell'array per ogni accesso diventa un problema di prestazioni. Quando gli array diventano più grandi, il problema delle prestazioni peggiora notevolmente. Inoltre, man mano che la tua app diventa più complessa, mostrando e filtrando i dati in più posizioni, anche il problema peggiora. La combinazione può essere dannosa. Accedendo agli elementi di id, il tempo di accesso cambia da O(n)a O(1), il che per gli elementi di grandi dimensioni n(in questo caso gli elementi dell'array) fa un'enorme differenza.

Q2: puoi utilizzare normalizrper aiutarti con la conversione da API a negozio. A partire da normalizr V3.1.0 puoi usare denormalize per andare dall'altra parte. Detto questo, le app sono spesso più consumatori che produttori di dati e come tale la conversione in negozio viene solitamente eseguita più frequentemente.

D3: I problemi che incontrerai utilizzando un array non sono tanto problemi con la convenzione di archiviazione e / o incompatibilità, ma più problemi di prestazioni.


normalizer è di nuovo la cosa che creerebbe sicuramente dolore una volta cambiate le def nel backend. Quindi questo deve essere aggiornato ogni volta
Kunal Navhate

12

Pensa allo stato dell'app come a un database.

Questa è l'idea chiave.

1) Avere oggetti con ID univoci ti consente di usare sempre quell'ID quando fai riferimento all'oggetto, quindi devi passare la quantità minima di dati tra azioni e riduttori. È più efficiente rispetto all'utilizzo di array.find (...). Se usi l'approccio ad array devi passare l'intero oggetto e questo può diventare disordinato molto presto, potresti finire per ricreare l'oggetto su diversi riduttori, azioni o persino nel contenitore (non lo vuoi). Le viste saranno sempre in grado di ottenere l'oggetto completo anche se il loro riduttore associato contiene solo l'ID, perché quando mappi lo stato otterrai la raccolta da qualche parte (la vista ottiene l'intero stato per mapparlo alle proprietà). A causa di tutto ciò che ho detto, le azioni finiscono per avere la quantità minima di parametri e riducono la quantità minima di informazioni, provaci,

2) La connessione all'API non dovrebbe influire sull'architettura del tuo archivio e dei riduttori, ecco perché hai azioni, per mantenere la separazione delle preoccupazioni. Basta inserire la logica di conversione dentro e fuori l'API in un modulo riutilizzabile, importare quel modulo nelle azioni che utilizzano l'API e dovrebbe essere così.

3) Ho usato array per strutture con ID, e queste sono le conseguenze impreviste che ho subito:

  • Ricreare costantemente oggetti in tutto il codice
  • Passaggio di informazioni non necessarie a riduttori e azioni
  • Di conseguenza, codice cattivo, non pulito e non scalabile.

Ho finito per modificare la struttura dei dati e riscrivere molto codice. Sei stato avvertito, per favore non metterti nei guai.

Anche:

4) La maggior parte delle raccolte con ID ha lo scopo di utilizzare l'ID come riferimento all'intero oggetto, dovresti trarne vantaggio. Le chiamate API riceveranno l'ID e quindi il resto dei parametri, così come le tue azioni e i tuoi riduttori.


Mi imbatto in un problema in cui abbiamo un'app con molti dati (da 1000 a 10.000) memorizzati da ID in un oggetto nel redux store. Nelle viste, utilizzano tutti array ordinati per visualizzare i dati delle serie temporali. Ciò significa che ogni volta che viene eseguito un rendering, deve prendere l'intero oggetto, convertirlo in un array e ordinarlo. Mi è stato assegnato il compito di migliorare le prestazioni dell'app. È un caso d'uso in cui ha più senso memorizzare i dati in un array ordinato e utilizzare la ricerca binaria per eliminare e aggiornare invece di un oggetto?
William Chou

Ho finito per dover creare altre mappe hash derivate da questi dati per ridurre al minimo il tempo di calcolo sugli aggiornamenti. Ciò fa sì che l'aggiornamento di tutte le diverse viste richieda la propria logica di aggiornamento. Prima di ciò, tutti i componenti avrebbero preso l'oggetto dall'archivio e avrebbero ricostruito le strutture dati necessarie per visualizzarlo. L'unico modo in cui riesco a pensare per garantire il minimo jankiness nell'interfaccia utente è utilizzare un web worker per eseguire la conversione da oggetto a matrice. Il compromesso per questo è il recupero più semplice e la logica di aggiornamento poiché tutti i componenti dipendono solo da un tipo di dati da leggere e scrivere.
William Chou

8

1) La semplicità del riduttore è la motivazione per seguire l'approccio object-keyed-by-id? Ci sono altri vantaggi in quella forma di stato?

Il motivo principale per cui si desidera mantenere le entità negli oggetti archiviati con ID come chiavi (chiamato anche normalizzato ), è che è davvero complicato lavorare con oggetti profondamente nidificati (che è ciò che si ottiene in genere dalle API REST in un'app più complessa) - sia per i tuoi componenti che per i tuoi riduttori.

È un po 'difficile illustrare i vantaggi di uno stato normalizzato con il tuo esempio attuale (poiché non hai una struttura profondamente nidificata ). Ma diciamo che le opzioni (nel tuo esempio) avevano anche un titolo e sono state create dagli utenti nel tuo sistema. Ciò renderebbe invece la risposta simile a questa:

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

Supponiamo ora che tu voglia creare un componente che mostri un elenco di tutti gli utenti che hanno creato opzioni. Per fare ciò, devi prima richiedere tutti gli elementi, quindi iterare su ciascuna delle loro opzioni e infine ottenere il created_by.username.

Una soluzione migliore sarebbe normalizzare la risposta in:

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 questa struttura, è molto più semplice ed efficiente elencare tutti gli utenti che hanno creato opzioni (li abbiamo isolati in entity.optionCreators, quindi dobbiamo solo scorrere l'elenco).

È anche abbastanza semplice mostrare, ad esempio, i nomi utente di coloro che hanno creato opzioni per l'elemento filtro con ID 1:

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

2) Sembra che l'approccio object-keyed-by-id renda più difficile gestire l'ingresso / uscita JSON standard per un'API. (Ecco perché ho scelto l'array di oggetti in primo luogo.) Quindi, se segui questo approccio, usi semplicemente una funzione per trasformarlo avanti e indietro tra il formato JSON e il formato della forma dello stato? Sembra goffo. (Anche se sostenete questo approccio, fa parte del vostro ragionamento che è meno goffo del riduttore di array di oggetti sopra?)

Una risposta JSON può essere normalizzata utilizzando ad esempio normalizr .

3) So che Dan Abramov ha progettato il redux per essere teoricamente indipendente dalla struttura dei dati di stato (come suggerito da "Per convenzione, lo stato di primo livello è un oggetto o qualche altra raccolta di valori-chiave come una mappa, ma tecnicamente può essere qualsiasi tipo ", enfasi mia). Ma dato quanto sopra, è semplicemente "consigliato" mantenerlo un oggetto codificato da ID, o ci sono altri punti deboli imprevisti in cui mi imbatterò usando una serie di oggetti che lo rendono tale che dovrei semplicemente interromperlo pianificare e provare a restare con un oggetto con chiave ID?

Probabilmente è una raccomandazione per app più complesse con molte risposte API profondamente nidificate. Nel tuo esempio particolare, però, non ha molta importanza.


1
maprestituisce undefined come qui , se le risorse vengono recuperate separatamente, rendendo filtertroppo complicato. C'è una soluzione?
Saravanabalagi Ramachandran

1
@tobiasandersen pensi che sia ok per il server restituire dati normalizzati ideali per React / Redux, per evitare che il client esegua la conversione tramite libs come normalizr? In altre parole, fare in modo che il server normalizzi i dati e non il client.
Matteo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.