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_FILTER
część 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?
źródło
sort_by
?const sorted = _.sortBy(collection, 'attribute');
Odpowiedzi:
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 przezid
, czas dostępu zmienia się zO(n)
naO(1)
, co w przypadku dużychn
(tutaj elementów tablicy) ma ogromne znaczenie.P2: Możesz użyć,
normalizr
aby 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ą.
źródło
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:
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.
źródło
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)
Odpowiedź JSON można znormalizować za pomocą np . Normalizr .
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.
źródło
map
zwraca undefined, tak jak tutaj , jeśli zasoby są pobierane oddzielnie, co czynifilter
s zbyt skomplikowanymi. Czy jest jakieś rozwiązanie?