Manipulowanie obiektami w Reduxie może początkowo wydawać się nieco przytłaczające, ale z czasem staje się bardzo jasne i łatwe do zrozumienia. Wystarczy zapamiętać kilka rzeczy i nie można zapomnieć o kluczowej zasadzie – nie mutuj bezpośrednio stanu.

Celem tego artykułu jest pokazanie, jak manipulować zagnieżdżonymi obiektami i tablicami w Reduxie, więc nie będę skupiać się na takich rzeczach, jak konfigurowanie projektu React i konfigurowanie Reduxa. Nie będę się też skupiał na akcjach reduxowych. Na potrzeby tego artykułu stworzyłem prosty projekt na Github – stronę z postami, komentarzami i polubieniami.

https://github.com/wojciechnowaczyk/reduxApp

Co już powinieneś wiedzieć: 

  • podstawy JS
  • podstawy React
  • podstawy Redux

Do dzieła!

Mutowanie stanu

Obiekty w stanie Reduxa są niezmienne, co oznacza, że wartości nie mogą być zmieniane i aby poradzić sobie z tą sytuacją wystarczy utworzyć kopie elementów ze zmienionymi wartościami. Porównanie obiektów odbywa się za pomocą tzw. shallow compare, co oznacza, że porównywane są tylko odniesienia do obiektów, a nie cała struktura. W tym przypadku dodawanie, usuwanie i zmienianie pól nie zmienia referencji, ale strukturę.

Dlaczego powinniśmy unikać mutacji stanu? Z oficjalnej dokumentacji Redux:

  • Powoduje błędy, takie jak nieprawidłowe aktualizowanie interfejsu użytkownika w celu wyświetlenia najnowszych wartości
  • Utrudnia zrozumienie, dlaczego i w jaki sposób stan został zaktualizowany
  • Utrudnia pisanie testów
  • Przerywa możliwość prawidłowego korzystania z “time-travel debugging”
  • Jest to sprzeczne z zamierzonym duchem i wzorcami użytkowania Redux

Inkrementacja

Pierwszym i moim zdaniem najłatwiejszym do pokazania przypadkiem jest tworzenie akcji inkrementacji. Stworzyłem strukturę stanu, która wygląda tak:

Należy zadać sobie pytanie, co tak naprawdę chcemy osiągnąć? Naszym celem jest zwiększenie “likesCount” po kliknięciu w przycisk. W tym przypadku musimy przekazać id postu jako argument do akcji dispatch. Musimy wiedzieć, który obiekt zamierzamy zmienić.

Pierwszą rzeczą, którą musimy zrobić, to zwrócić cały stan, aby uniknąć mutacji – chcemy zmienić pojedynczy element, a nie nadpisywać cały stan, prawda?

Aby to zrobić, wystarczy zwrócić “…state” (te trzy kropki to spread operator, więcej na ten temat przeczytasz tutaj.)

Jeśli tak zostawimy kod, akcja nie spowoduje żadnych zmian – zostanie utworzona kopia stanu z tymi samymi wartościami (bez żadnych zmian). A my chcemy coś zmienić! Chcemy dodać polubienie do likesCount.

Następnym krokiem jest więc przefiltrowanie tablicy postów i znalezienie tego, który chcemy zmienić na podstawie id posta, który przekazaliśmy w akcji dispatch.

Ostatnią rzeczą, którą musimy zrobić jest zwiększenie wartości. Po pierwsze, musimy zwrócić pozostałe elementy posta za pomocą “…post”. Sytuacja jest podobna do zwrócenia całego stanu na początku. Teraz możemy uzyskać dostęp do atrybutu, który chcemy zmienić i zwiększyć wartość o jeden.

I dobiliśmy do brzegu! Licznik polubień powinien działać poprawnie. Pamiętaj, że na potrzeby tego artykułu użytkownik może nieskończenie zwiększać liczbę likesCount. W rzeczywistych, komercyjnych projektach w większości scenariuszy powinna istnieć możliwość dodania tylko jednego polubienia.

Dekrementacja

Sposób w jaki można zaimplementować dekrementację jest podobny do inkrementacji z jedną różnicą – wystarczy odjąć wartość w likesCount. Możesz to zobaczyć poniżej:

Dodawania obiektu do tablicy

Przyjrzyjmy się teraz komentarzom i dodajmy nowe komentarze do listy. Nasz obiekt komentarza wygląda tak:

więc jeśli chcemy dodać nowy komentarz do listy wystarczy dodać obiekt zgodny z tym wzorcem.

Po pierwsze, podobnie jak w poprzednich przypadkach, wystarczy zwrócić {…state}, aby uniknąć mutacji. Mamy listę wszystkich postów, ale chcemy tylko zmienić komentarze do jednego posta, więc co powinniśmy zrobić? Wystarczy wykonać funkcję map() na tablicy posts i porównać identyfikator posta z identyfikatorem przekazanym w akcji. Jeśli identyfikatory są sobie równe, możemy manipulować tym obiektem. Pozostałe obiekty są zwracane bez zmian.

Kiedy już znajdziemy konkretny post, który chcemy zmienić, co powinniśmy zrobić dalej? Pierwszą rzeczą, którą musimy zrobić jest jak zawsze, zwrócenie całego obiektu {…post}, a następnie po prostu wpisanie: 

“comments: post.comments.concat(newComment)”. 

Nowy komentarz to obiekt, który może zostać przekazany z akcji lub ustawiony w reducerze. Jest to nowy obiekt komentarza, który zawiera pola id i tekstowe. A co z funkcją concat()?

Z oficjalnej dokumentacji:

“Metoda concat() służy do łączenia dwóch lub więcej tablic. Ta metoda nie zmienia istniejących tablic, ale zamiast tego zwraca nową tablicę.” Jeśli chcesz przeczytać więcej o tej metodzie, po prostu odwiedź ten link.

I to wszystko. Do posta dodaliśmy nowy komentarz!

Usuwanie obiektu z tablicy

Usuwanie komentarzy można zrobić w nieco inny sposób. Aby usunąć komentarz, potrzebujemy indeksu elementu, który chcemy usunąć. Aby to osiągnąć, musimy pobrać obiekt z postem, którym chcemy zmienić, znaleźć indeks komentarza, który chcemy usunąć, i zrobić to za pomocą metody slice.

Aby znaleźć obiekt możemy użyć metody find JS:

“Metoda find() zwraca wartość pierwszego elementu w podanej tablicy, który spełnia podaną funkcję. Jeśli żadna wartość nie spełnia funkcji, zwracana jest wartość undefined.” Więcej o tej metodzie znajdziesz tutaj.

Następnie na obiekcie, który zwraca metoda find, używamy metody findIndex JS:

„Metoda findIndex() zwraca indeks pierwszego elementu w tablicy, który spełnia podaną funkcj. W przeciwnym razie zwraca wartość -1, co oznacza, że żaden element nie przeszedł testu”. Więcej o tej metodzie znajdziesz tutaj. 

Musimy znaleźć obiekt na podstawie id komentarza w porównaniu z id przekazanym w akcji; przekazujemy funkcję wewnątrz metody findIndex: 

posts.comments.findIndex(comment => comment.id === action.payload.id).

Gdy otrzymamy indeks elementu, który chcemy usunąć, wystarczy użyć metody slice:

„Metoda slice() zwraca płytką kopię części tablicy do nowego obiektu tablicy wybranego od początku do końca (bez końca), gdzie początek i koniec reprezentują indeks elementów w tej tablicy. Oryginalna tablica nie będzie zmodyfikowany.”. Więcej o metodzie tutaj. 

Jak stworzymy nową tablicę komentarzy? Musimy tylko stworzyć nową tablicę na podstawie poprzedniej.

const newCommentsArray = […post.comments.slice(0, index), …post.comments.slice(index + 1)].

W skrócie –  łączymy dwie tablice w jedną, pomijając jeden element, który chcemy usunąć.

…post.comments.slice(0, index) – zwraca tablicę od początku do ostatniego elementu przed indeksem, który chcemy usunąć.
…post.comments.slice(index + 1) – zwraca tablicę od pierwszego elementu po elemencie, który chcemy usunąć, do końca tablicy.

Podsumowanie

Jak widać, updateowanie obiektów w Reduxie nie jest tak trudne, jak się wydaje. Musisz tylko pamiętać, aby nie mutować stanu i być bardzo skupionym na modyfikowaniu zagnieżdżonych obiektów – musisz mieć pewność, że próbujesz zmienić odpowiedni obiekt. Oczywiście istnieje wiele sposobów tego, jak powinna wyglądać struktura Redux. Istnieją różne sposoby zmiany zagnieżdżonych obiektów, ale chciałem pokazać tylko ten najbardziej powszechny. Jeśli chcesz uprościć akcje reduxowe, możesz spojrzeć na Immer – bibliotekę, która upraszcza proces pisania niezmiennej logiki aktualizacji. Aby to zrobić, wystarczy zaimportować funkcję produce z biblioteki Immer, przekazać aktualny stan i przekazać zmiany, które powinny zostać wprowadzone. Biblioteka zajęłaby się problemem mutowania stanu! Biblioteka Immer wygląda bardzo pomocnie, ale warto też wiedzieć, co dzieje się wewnątrz reducerów!

Wojciech Nowaczyk