Home

Moje dobre praktyki w React.js

Ciężko mi już zliczyć ile projektów w React (…i jego ekosystemie) napisałem. Postanowiłem spisać praktyki które wypracowałem i uważam za warte użycia w waszych projektach.

Unikanie lokalnego stanu

Naturalne zdaje się posiadanie pewnej architektury stanu, co w projektach Reactowych przeważnie oznacza Redux, MobX lub jakąś inną, mniej popularną implementację. Ważne jest, że przeniesienie całego stanu w jedno miejsce sprawia że aplikacja staje się przewidywalna i łatwa do debugowania, a gdy nowy programista potrzebuje dowiedzieć się skąd pochodzą dane, zawsze wie gdzie szukać.

Używanie stanu Reactowego (hook useState lub state w class komponentach) ma swoje uzasadnienia, jednak w większości przypadków powoduje komplikacje cyklu rerenderowania, tworzy tzw. side effecty i w efekcie mamy dwa różne miejsca gdzie tego stanu szukać.

Jeżeli lokalnego stanu używam, to w bardzo określonych przypadkach – np w formularzach (korzystając z abstrakcji Formika) czy niektórych komponentów UI, które mogą być samodzielne – accordiony czy karuzele.

Abstrakcja stanu od widoku

Gdy już użyję stanu to zawsze staram się oddzielić go od dumb componentu.

Przykładowo, jeżeli mam komponent accordion i potrzebuję wiedzieć która sekcja jest otwarta, i tak tworzę czysty komponent (controlled), który w propsach dostaje aktualnie otwartą sekcję (np. ID) oraz callback do otwarcia pozostałych. Następnie tworzę HOC, który posiada stan (przeważnie hook useState) i owija nim Accordion, tworząc samodzielny komponent.

Po co ta gimnastyka?

Po pierwsze, dużo łatwiej testować te dwie funkcje osobno niż razem. Na końcu test „integracyjny” oczywiście można przeprowadzić na owrapowanym komponencie.

Po drugie, jest spora szansa że wraz z nadejściem nowych wymagań biznesowych, sekcje będą otwarte np. zależnie od stanu URL czy innych komponentów, co pozwala mi ponownie użyć czystego komponentu bez modyfikowania tego stanowego.

Używanie selectorów w Reduxie za każdym razem

Standardowe (tutorialowe) zastosowania funkcji mapStateToProps bezpośrednio w warstwie komponentu tworzy mapowanie stanu na propsy, tworząc tight coupling, a proces mapowania powtarzany jest za każdym razem gdy potrzeba danych.

Jeżeli zmienimy strukturę naszego store Reduxa, musimy zmienić to w każdym mapStateToProps, którego użyjemy, niezbyt DRY.

Dlatego nawet do prostych mapowań zawsze używam selectorów (Reselect, memoize-one), nawet jeżeli jest to prosta przelotka (x => x). Selectory dają dodatkowo… nazwę, która mówi nam ładnie i klarownie co pobieramy (getDistanceFormattedInMeters itd). Oczywiście nie trzeba mówić o zaletach memoizacji, która zapobiega niepotrzebnym rerenderom poprzez cache wyników.

Używam Typescript

Jest to uniwersalna dobra praktyka i nie będę się specjalnie nad zaletami TS rozwodził. W kontekście samego Reacta wspomnę tylko o dużo przyjemniejszym i „naturalnym” typowaniu propsów niż prop-types. Ponadto można otypować jakie komponenty można przekazać do HOC-a.

Obiekt zamiast tablicy w Reduxowym modelu danych

Przez długi czas moje struktury danych w Reduxie były raczej tablicami, np. { posts: Post[] }, jednak wraz ze skalowaniem aplikacji zauważyłem dużo większe benefity w strukturze klucz-wartość, np. { postsById: { [postId: number]: Post } }. Struktura ta pozwala dużo łatwiej aktualizować i pobierać dane, bez kosztownych operacji na tablicach.

Finalnie używam selektora, który mapuje dane do tablic, przykładowo:

const getPosts = state => state.posts.postsById;

const getPosts = (): Post[] => createSelector(
   getPosts,
   posts => Object.values(posts)
);

Przekazuję komponenty jako propsy

Technika ta szczególnie przydaje się do tworzenia uniwersalnych, reużywalnych bibliotek UI (zerknij na Material UI). Standardowym scenariuszem jest stworzenie komponentu na bazie określonego designu, a potem wraz z nowymi wymaganiami zaczynają się komplikacje – ściany ifów, ogromne kombinacje propsów i tworzenie wielu różnych wariantów.

Dlatego staram się dostrzec w komponentach coś w rodzaju slotów. Gdy widzę Button z ikoną po lewej (lub prawej) stronie, tworzę komponent który przyjmuje opcjonalny ReactNode w propsach leftSlot i rightSlot (lub leftIcon i rightIcon, to już zależy).

Późniejsze użycie jest bardzo przyjemne, bo przy użyciu kompozycji tworzę sobie różne reużywalne warianty, przykładowo:

const ContinueButton = withProps({
  rightIcon: <DashRightIcon />
})(Button);

const CancelButton = withProps({
  leftIcon: <CancelIcon />
})(Button);

Tego typu przykładów mam dziesiątki i nie trzeba wcale pisać reużywalnej biblioteki by warto było skupić się na takim podejściu. Przede wszystkim zapewnia nam to większe bezpieczeństwo w przypadku wszelkich zmian, a te w większych projektach są nieuniknione.

Wszelkie efekty staram się przekazywać przez Reduxa

W standardowej aplikacji typowym efektem są wszelkie requesty HTTP. Piszę serwisy, które odpowiadają za przesyłanie danych, jednak w żadnym wypadku nie używam serwisów w komponentach. Używam do tego efektów, a więc komponenty łączą się jedynie z Reduxem. Efekty zwracają przeważnie Promise-y, a te obsługuję zarówno w modelu (wywołanie reducera, zapisanie danych) jak i później w komponencie (zareagowanie na wynik).

Tego typu reguła gwarantuje mi że wszelkich side effectów mogę szukać w modelu, zamiast rozprzestrzenionych po całej aplikacji na różnych warstwach.

Tworzę moduły domenowe

Nie ma dla mnie nic gorszego niż podział na wielkie foldery typu components, containers, actions, reducers etc. To rozwiązanie się nie skaluje, więc staram się tworzyć moduły związane z określonymi domenami. Nie jest to łatwe, a czasem, jeśli nie znamy dobrze projektu (lub zachodzą w nim częste zmiany) możemy narazić się na dodatkowy refactoring. Jednak warto jest tworzyć modułowe struktury, tak jak robi to Angular (pisałem o tym jak Angular pomógł mi stać się lepszym React developerem). Warto spojrzeć na Ducks.

Typowa zawartość modułu to interfejsy ze strukturami danych, serwisy, modele (store Redux), strony/widoki oraz komponenty związane ściśle z tymi domenami.

To tyle co mi przyszło do głowy, kiedyś powstanie kolejna część 🙂