Stan jako tablica obiektów a obiekt z kluczem według id

97

W rozdziale dotyczącym projektowania kształtu stanu , dokumenty sugerują, aby zachować stan w obiekcie z kluczem identyfikacyjnym:

Zachowaj każdą jednostkę w obiekcie przechowywanym z identyfikatorem jako kluczem i używaj identyfikatorów do odwoływania się do niej z innych jednostek lub list.

Dalej stwierdzają

Pomyśl o stanie aplikacji jak o bazie danych.

Pracuję nad kształtem stanu dla listy filtrów, z których niektóre będą otwarte (są wyświetlane w wyskakującym okienku) lub będą miały wybrane opcje. Kiedy przeczytałem „Pomyśl o stanie aplikacji jak o bazie danych”, pomyślałem o tym, że myślę o nich jako o odpowiedzi JSON, która byłaby zwracana z interfejsu API (sam w sobie wspierany przez bazę danych).

Więc myślałem o tym jako

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

Jednak dokumenty sugerują bardziej podobny format

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

W teorii nie powinno to mieć znaczenia, o ile dane można serializować (pod nagłówkiem „Stan”) .

Więc szczęśliwie poszedłem z podejściem tablicy obiektów, dopóki nie pisałem reduktora.

W przypadku podejścia z kluczem obiektowym według identyfikatora (i liberalnego użycia składni rozproszenia) OPEN_FILTERczęść reduktora staje się

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

Podczas gdy w przypadku podejścia opartego na tablicy obiektów jest to bardziej szczegółowe (i zależne od funkcji pomocniczych)

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

Więc moje pytania są trojakie:

1) Czy prostota reduktora jest motywacją do zastosowania podejścia z kluczem obiektowym według identyfikatora? Czy są inne zalety tego stanu?

i

2) Wygląda na to, że podejście z kluczem obiektowym według identyfikatora utrudnia obsługę standardowego wejścia / wyjścia JSON dla interfejsu API. (Dlatego w pierwszej kolejności wybrałem tablicę obiektów). Jeśli więc zastosujesz takie podejście, czy po prostu użyjesz funkcji, aby przekształcić ją tam iz powrotem między formatem JSON a formatem kształtu stanu? To wydaje się niezgrabne. (Chociaż jeśli popierasz takie podejście, czy po części rozumujesz, że jest to mniej niezgrabne niż powyższy reduktor tablicy obiektów?)

i

3) Wiem, że Dan Abramov zaprojektował redukcję, aby teoretycznie być agnostykiem stanu-danych-struktury (jak sugeruje „Zgodnie z konwencją, stan najwyższego poziomu to obiekt lub inna kolekcja klucz-wartość, taka jak mapa, ale technicznie może to być dowolna tekst , wyróżnienie moje). Ale biorąc pod uwagę powyższe, czy jest to tylko "zalecane", aby zachować obiekt z kluczem ID, czy też są inne nieprzewidziane problemy, na które napotkam używając tablicy obiektów, które sprawiają, że powinienem po prostu przerwać planować i próbować trzymać się obiektu z kluczem identyfikacyjnym?

nickcoxdotme
źródło
2
Jest to interesujące pytanie, które również miałem, aby zapewnić trochę wglądu, chociaż mam tendencję do normalizowania reduxów zamiast tablic (tylko dlatego, że wyszukiwanie jest łatwiejsze), stwierdzam, że jeśli przyjmiesz znormalizowane podejście, sortowanie staje się problem, ponieważ nie masz takiej samej struktury, jaką daje tablica, więc jesteś zmuszony do sortowania siebie.
Robert Saunders,
Widzę problem w podejściu „object-keyed-by-id”, jednak nie jest to częste, ale musimy wziąć to pod uwagę podczas pisania dowolnej aplikacji UI. A co jeśli chcę zmienić kolejność encji za pomocą elementu przeciągnij i upuść wymienionego na liście uporządkowanej? Zwykle podejście typu „object-keyed-by-id” zawodzi tutaj i na pewno wybrałbym podejście z tablicą obiektów, aby uniknąć takich hojnych problemów. Mogłoby być więcej, ale myślę o udostępnieniu tego tutaj
Kunal Navhate
Jak możesz sortować obiekt złożony z przedmiotów? Wydaje się to niemożliwe.
David Vielhuber
@DavidVielhuber Masz na myśli oprócz używania czegoś takiego jak lodash sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme
Tak. Obecnie konwertujemy te obiekty na tablice wewnątrz obliczonej właściwości vue
David Vielhuber

Odpowiedzi:

47

P1: Prostota reduktora wynika z braku konieczności przeszukiwania tablicy w celu znalezienia odpowiedniego wpisu. Zaletą jest brak konieczności przeszukiwania tablicy. Selektory i inne podmioty udostępniające dane mogą i często mają dostęp do tych elementów za pomocą id. Konieczność przeszukiwania macierzy dla każdego dostępu staje się problemem z wydajnością. Gdy tablice stają się większe, problem z wydajnością gwałtownie się pogarsza. Ponadto, gdy Twoja aplikacja staje się bardziej złożona, wyświetlając i filtrując dane w większej liczbie miejsc, problem również się pogarsza. Połączenie może być szkodliwe. Uzyskując dostęp do elementów przez id, czas dostępu zmienia się z O(n)na O(1), co w przypadku dużych n(tutaj elementów tablicy) ma ogromne znaczenie.

P2: Możesz użyć, normalizraby pomóc Ci w konwersji z API do sklepu. Od wersji normalizr V3.1.0 możesz użyć denormalize, aby przejść w drugą stronę. To powiedziawszy, aplikacje są często bardziej konsumentami niż producentami danych, w związku z czym konwersja do sklepu jest zwykle wykonywana częściej.

P3: Problemy, na które napotkasz podczas korzystania z macierzy, to nie tyle problemy z konwencją przechowywania i / lub niezgodnościami, ale raczej problemy z wydajnością.

DDS
źródło
normalizator to znowu rzecz, która z pewnością spowodowałaby ból, gdy zmienimy defki w zapleczu. Więc to musi być zawsze aktualne
Kunal Navhate
12

Pomyśl o stanie aplikacji jak o bazie danych.

To jest kluczowa idea.

1) Posiadanie obiektów z unikalnymi identyfikatorami pozwala zawsze używać tego identyfikatora podczas odwoływania się do obiektu, więc musisz przekazywać minimalną ilość danych między akcjami i redukcjami. Jest to bardziej wydajne niż użycie array.find (...). Jeśli korzystasz z podejścia tablicowego, musisz przekazać cały obiekt, a to może szybko stać się bałaganiarskie, możesz w końcu odtworzyć obiekt na różnych reduktorach, akcjach, a nawet w kontenerze (nie chcesz tego). Widoki zawsze będą w stanie uzyskać pełny obiekt, nawet jeśli skojarzony z nimi reduktor zawiera tylko identyfikator, ponieważ podczas mapowania stanu gdzieś otrzymasz kolekcję (widok pobierze cały stan, aby zmapować go do właściwości). W związku z tym, co powiedziałem, działania kończą się na minimalnej ilości parametrów i zmniejszają minimalną ilość informacji, spróbuj,

2) Połączenie z API nie powinno wpływać na architekturę twojego magazynu i reduktorów, dlatego masz działania, aby zachować separację problemów. Po prostu umieść logikę konwersji w interfejsie API i poza nim w module wielokrotnego użytku, zaimportuj ten moduł w akcjach korzystających z interfejsu API i to powinno być to.

3) Użyłem tablic dla struktur z identyfikatorami, a są to nieprzewidziane konsekwencje, których doświadczyłem:

  • Ciągłe odtwarzanie obiektów w całym kodzie
  • Przekazywanie zbędnych informacji reduktorom i akcjom
  • Konsekwencją tego jest zły, nieczysty i nieskalowalny kod.

Skończyło się na zmianie struktury danych i przepisaniu dużej ilości kodu. Zostałeś ostrzeżony, nie wpadaj w kłopoty.

Również:

4) Większość kolekcji z identyfikatorami ma na celu używanie identyfikatora jako odniesienia do całego obiektu, należy to wykorzystać. Wywołania API otrzymają identyfikator, a następnie pozostałe parametry, podobnie jak Twoje akcje i redukcje.

Marco Scabbiolo
źródło
Napotykam problem polegający na tym, że mamy aplikację z dużą ilością danych (od 1000 do 10000) przechowywanych przez identyfikator w obiekcie w sklepie Redux. W widokach wszyscy używają posortowanych tablic do wyświetlania danych szeregów czasowych. Oznacza to, że za każdym razem, gdy wykonywane jest ponowne wyrenderowanie, musi on pobrać cały obiekt, przekonwertować go na tablicę i posortować. Miałem za zadanie poprawić wydajność aplikacji. Czy jest to przypadek użycia, w którym bardziej sensowne jest przechowywanie danych w posortowanej tablicy i używanie wyszukiwania binarnego do usuwania i aktualizacji zamiast obiektu?
William Chou
Skończyło się na tym, że musiałem utworzyć inne mapy skrótów pochodzące z tych danych, aby zminimalizować czas obliczeń podczas aktualizacji. To sprawia, że ​​aktualizacja wszystkich różnych widoków wymaga ich własnej logiki aktualizacji. Wcześniej wszystkie komponenty pobierały obiekt z magazynu i przebudowywały struktury danych potrzebne do wyświetlenia. Jedynym sposobem, w jaki mogę wymyślić, aby zapewnić minimalne szarpanie w interfejsie użytkownika, jest użycie programu roboczego do wykonania konwersji z obiektu na tablicę. Kompromisem jest prostsze pobieranie i aktualizowanie logiki, ponieważ wszystkie komponenty zależą tylko od jednego typu danych do odczytu i zapisu.
William Chou
8

1) Czy prostota reduktora jest motywacją do zastosowania podejścia z kluczem obiektowym według identyfikatora? Czy są inne zalety tego stanu?

Głównym powodem, dla którego chcesz zachować encje w obiektach przechowywanych z identyfikatorami jako kluczami (zwanymi również znormalizowanymi ), jest to, że praca z głęboko zagnieżdżonymi obiektami jest naprawdę uciążliwa (co zwykle uzyskuje się z interfejsów API REST w bardziej złożonej aplikacji) - zarówno dla twoich komponentów, jak i reduktorów.

Trochę trudno jest zilustrować zalety znormalizowanego stanu na obecnym przykładzie (ponieważ nie masz głęboko zagnieżdżonej struktury ). Ale powiedzmy, że opcje (w twoim przykładzie) również miały tytuł i zostały utworzone przez użytkowników w twoim systemie. Zamiast tego odpowiedź wyglądałaby mniej więcej tak:

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

Teraz powiedzmy, że chcesz utworzyć komponent pokazujący listę wszystkich użytkowników, którzy utworzyli opcje. Aby to zrobić, musisz najpierw zażądać wszystkich elementów, a następnie powtórzyć każdą z ich opcji, a na końcu uzyskać nazwę created_by.username.

Lepszym rozwiązaniem byłoby znormalizowanie odpowiedzi na:

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

Dzięki tej strukturze znacznie łatwiej i wydajniej jest wyświetlić listę wszystkich użytkowników, którzy utworzyli opcje (mamy je wyizolowane w entity.optionCreators, więc musimy po prostu przejrzeć tę listę).

Dość łatwo jest również pokazać np. Nazwy użytkowników tych, którzy utworzyli opcje dla elementu filtru o identyfikatorze 1:

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

2) Wygląda na to, że podejście z kluczem obiektowym według identyfikatora utrudnia obsługę standardowego wejścia / wyjścia JSON dla interfejsu API. (Dlatego w pierwszej kolejności wybrałem tablicę obiektów). Jeśli więc zastosujesz takie podejście, czy po prostu użyjesz funkcji, aby przekształcić ją tam iz powrotem między formatem JSON a formatem kształtu stanu? To wydaje się niezgrabne. (Chociaż jeśli popierasz takie podejście, czy po części rozumujesz, że jest to mniej niezgrabne niż powyższy reduktor tablicy obiektów?)

Odpowiedź JSON można znormalizować za pomocą np . Normalizr .

3) Wiem, że Dan Abramov zaprojektował redukcję, aby teoretycznie być agnostykiem stanu-danych-struktury (jak sugeruje „Zgodnie z konwencją, stan najwyższego poziomu to obiekt lub inna kolekcja klucz-wartość, taka jak mapa, ale technicznie może to być dowolna tekst, wyróżnienie moje). Ale biorąc pod uwagę powyższe, czy jest to tylko "zalecane", aby zachować obiekt z kluczem ID, czy też są inne nieprzewidziane problemy, na które napotkam używając tablicy obiektów, które sprawiają, że powinienem po prostu przerwać planować i próbować trzymać się obiektu z kluczem identyfikacyjnym?

Prawdopodobnie jest to zalecenie dla bardziej złożonych aplikacji z wieloma głęboko zagnieżdżonymi odpowiedziami API. Jednak w twoim konkretnym przykładzie nie ma to większego znaczenia.

tobiasandersen
źródło
2
mapzwraca undefined, tak jak tutaj , jeśli zasoby są pobierane oddzielnie, co czyni filters zbyt skomplikowanymi. Czy jest jakieś rozwiązanie?
Saravanabalagi Ramachandran
1
@tobiasandersen Czy uważasz, że serwer może zwracać znormalizowane dane idealne do reakcji / redukcji, aby uniknąć konwersji przez klienta za pośrednictwem bibliotek, takich jak normalizr? Innymi słowy, spraw, aby serwer znormalizował dane, a nie klient.
Matthew