Captain Hook. Redux Store z wykorzystaniem React Hooks
Wraz z nadejściem ery Web 2.0 strony internetowe przestały być jedynie prostymi dokumentami tekstowymi, a zaczęły być aplikacjami raczej przypominającymi aplikacje desktopowe. Użytkownicy dostali możliwość tworzenia i dostosowywania treści do własnych potrzeb. Z konsumentów Internetu przeobrazili się w jego twórców. Wiązało się to wieloma wyzwaniami, które stanęły przed programistami w związku z nowymi, prężnie rozwijającymi się technologiami.
Chcąc dostarczyć użytkownikom jak najwięcej możliwości aplikacje klienckie zaczęły się rozrastać, a poziom ich skomplikowania spowodował, że także frontendowcy musieli zacząć myśleć o przechowywaniu i przepływie danych po stronie klientów. Co jednak zrobić, kiedy danych jest naprawdę dużo i stają się trudne do utrzymywania a ich przepływ niemożliwy do prześledzenia?
Tomasz Podkowski. Frontend Developer w firmie Merixstudio, gdzie chętnie podejmuje się projektów związanych z realizacją złożonych zadań wymagających kreatywnego podejścia do kodowania. Na co dzień podnosi swoje umiejętności programowania w Angularze, ale zgłębia również tajniki Reacta. Po godzinach relaksuje się przesłuchując swoją obszerną kolekcję płyt CD.
Z propozycją rozwiązania tego problemu wyszedł sam Facebook, prezentując architekturę Flux, która sama w sobie jest raczej wzorcem pisania kodu, niż pełnoprawnym frameworkiem. Dzięki zastosowaniu jednokierunkowego przepływu danych, pozwala na jego dokładne prześledzenie i przeanalizowanie, a co za tym idzie zdecydowanie ułatwia pracę z kodem i jego późniejsze utrzymanie.
Jej pełnoprawnym zastosowaniem, dostarczającym API, które zostało oficjalnie wsparte przez twórcę Fluxa jest Redux. Niewielki framework (~2kB włączając w to zależności). Choć najczęściej kojarzony z Reactem, brak jest jakichkolwiek przeciwwskazań do wykorzystania go w parze z jakąkolwiek inną biblioteką do tworzenia UI.
Spis treści
Redux
Redux przechowuje stan całej aplikacji w strukturze drzewa obiektów w tzw. store. Dzięki pojedynczemu storowi, naszemu Single Source of Truth całej aplikacji, jesteśmy w stanie łatwo debugować i podglądać cały przepływ danych. Wszelkie zmiany wykonywane są jedynie za pomocą pure functions gdyż w celu zablokowania mutacji stora przez side-effecty ten jest skonfigurowany tak, aby był “tylko do odczytu”.
Akcje, Typy i Payload
Akcja to zwykły obiekt, którego zadaniem jest opisanie co stało się w aplikacji. To przez dispatch akcji informujemy o zmianie stanu aplikacji. Każda akcja powinna być opisana przez typ – string opisujący do czego dana akcja służy.
dispatch({ type: 'INCREMENT' }); dispatch({ type: 'SET_COUNTER', counter: 1 });
Reducers
Żeby powiązać stan z akcjami tworzymy reducery. To nic innego jak funkcje reagujące na każdą dispatchowaną akcję. Przyjmują dwa argumenty: state oraz akcję. Nie modyfikują one bezpośrednio stanu, a jedynie zwracają jego zaktualizowaną kopię.
function counterReducer(state = [], action) { switch(action.type){ case 'INCREMENT': return { ...state, counter: state.counter += 1, } case 'SET_COUNTER': return { ...state, counter: action.counter, } default: return state; } }
Choć cała idea Reduxa wykorzystuje jedynie zwykły JavaScript, to jego integracja z daną biblioteką lub frameworkiem wymagać będzie wykorzystania konkretnego Redux API np. ng-redux czy react-redux. Czy możemy tego jakoś uniknąć? W Reactie tak.
Redux bez Reduxa?
React Hooks
Zaprezentowane w React 16.8. hooki w końcu pozwoliły użytkownikom tworzyć functional components bez obaw i problemów o zarządzaniem ich stanem. W końcu, aby móc korzystać ze state i lifecycle methods nie musimy już tworzyć class z ECMAScript 6. Hook to prosta funkcja, której parametrem jest wartość początkowa, a która zwraca listę dwóch elementów: pierwszym jest wartość, a drugim funkcja pozwalająca na zmianę tej wartości. Mała rzecz dająca spore możliwości.
const [counter, setCounter] = useState(initialCounter); const increment = () => setCounter(counter + 1); increment();
Redux + React Hooks?
Jak zatem wykorzystać hooki do stworzenia Redux API? Tym razem odwróćmy kolejność i zacznijmy od reducera.
function counterReducer(state = [], action) { switch (action.type) { case 'INCREMENT': return [...state, { counter: state.counter += 1, }]; case 'SET_COUNTER': return [...state, { counter: action.counter, }]; default: return state; } }
Powyższy reducer jest niemalże identyczny, jak ten nieprzeznaczony do współpracy z hookami. Jedyną różnicą pomiędzy nimi jest format w jakim zwracają dane. W pierwszym przypadku jest to po prostu state, natomiast w tym lista z dwoma elementami. Stwórzmy coś na kształt Redux API.
function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; }
Funkcja userReducer to nic innego jak custom hook, dzięki któremu uzyskujemy dostęp do statu oraz funkcji umożliwiającej jego zmianę. Oczywiście zanim to nastąpi dane kierowane są do reducera.
const [counter, dispatch] = useReducer(counterReducer, []); function handleCounterSet(counter) { dispatch({ type: 'SET_COUNTER', counter }); } handleCounterSet(1);
Twórcy Reacta przewidzieli, że zarządzanie data-flow naszej aplikacji przy wykorzystaniu reducerów jest na tyle częstym przypadkiem, że sami dostarczyli nam custom hook o nazwie useReducer.
const [state, dispatch] = useReducer(reducer, initialArg, init);
To w nim zawarli całą przedstawioną wcześniej logikę, dzięki czemu nie ma potrzeby pisania jej w całości od zera.
Zyski i straty
Dzięki wykorzystaniu React Hooks możemy stworzyć własną implementację Reduxa w naszej aplikacji. Nie musimy dzięki temu go pobierać i dodawać do projektu dodatkowych zależności, ale czy takie narzędzie będzie zawsze najlepszym wyborem? Odpowiem – pewnie nie. Redukujemy bundle o kilka kilobajtów, ale tracimy wszelką integrację choćby z takim narzędziem jak Redux DevTools. Wszystko więc jak zawsze zależy od projektu i wyobraźni programisty. Wybór technologii pozostawiam więc Wam.
Źródła:
Zdjęcie główne artykułu pochodzi z unsplash.com.