Elastyczność komponentów w React - kompozycja vs konfiguracja
- Czas potrzebny na przeczytanie:5 minut
- Opublikowane:
Podczas nauki React skupiamy się przede wszystkim na szczegółach technicznych, nic w tym złego. Poznajemy założenia, API biblioteki i cały ekosystem. W tym wszystkim często zapominamy o jednej, bardzo ważnej rzeczy - odpowiednim projektowaniu komponentów.
Komponent, czy całą bibliotekę komponentów można stworzyć na wiele różnych sposobów. Ja przedstawię Ci dzisiaj dwa podstawowe. Pomówimy o wadach i zaletach obu rozwiązań i o tym, co warto wybrać w zależności od przypadku.
Komponent "Modal"
Zacznijmy od pozornie prostego modala:
Konfiguracja
Pierwsze co przychodzi nam na myśl, to stworzenie jednego, generycznego komponentu i przekazanie mu odpowiednich propsów - Keep It Simple, Stupid.
<Modal
open={true}
onClose={true}
title="..."
description="..."
action={{
name: '...',
onClick: () => {},
}}
/>
Przekazaliśmy stan komponentu i potrzebne wartości. Można się rozjeść - zamykamy zadanie na Jirze i cieszymy się z szybkich efektów 🎉
Za jakiś czas przychodzi do nas Project Manager i przekazuje zadanie, w którym do stworzenia jest kolejny modal. Tym razem nie będzie tak łatwo...
Poza standardowymi title
i description
, możemy zauważyć, że zamiast jednego przycisku, są dwa. Dodatkowo, nasz Designer postanowił dodać przycisk do zamykania komunikatu. No nic, bierzemy się do pracy!
<Modal
open={true}
onClose={true}
header={{
title: '...',
icon: <CloseIcon />,
}}
description="..."
actions={[
{
name: '...',
onClick: () => {},
variant: 'red',
},
{
name: '...',
onClick: () => {},
variant: 'gray',
},
]}
/>
Nasz komponent zaczyna znacząco rosnąć - API jest bardzo sztywne i każde odstępstwo od pierwotnego designu będzie się wiązało z przekazywaniem kolejnych propsów i wariantów.
Im więcej propsów, tym zmniejsza się czytelność. Wraz z rozwojem takiego komponentu, możemy dojść do wniosku, że czytelniej będzie przekazać cały obiekt konfiguracyjny:
<Modal {...modalProps} />
Niestety jest to tylko "zaślepka" na sedno problemu.
Podsumujmy podejście oparte na bazie konfiguracji:
Zalety
Przewidywalność
API naszego komponentu jest jasne. Wiemy co musimy przekazać, żeby komponent działał prawidłowo.
Odwalamy część pracy za programistę - przekaż mi to i to, a ja dam Ci oczekiwany rezultat. Brak tutaj miejsca na błędy spowodowane nieprzekazaniem wymaganych wartości.
Wady
To się nie skaluje
Z czasem będą dochodzić kolejne warianty komponentu, elementy, dodatki. Jeśli dołożymy do tego warunkowe renderowanie, to wylądujemy w kropce. Nasz
JSX
zacznie puchnąć.Stwierdzimy, że czytelniej będzie przekazywać cały "obiekt konfiguracyjny", ale czy na pewno rozwiązuje nasz problem?
Brak elastyczności
Chcemy zmienić ułożenie pewnych elementów? Przyciski w danym wariancie powinny być ułożone nieco inaczej? Jesteśmy zmuszeni dodać kolejne propsy.
W pewnych przypadkach nie chcemy czegoś wyświetlać? Jesteśmy w stanie to zrobić, ale jakim kosztem? Możemy zapomnieć wtedy o "sztywnym" podejściu i trzymaniem programisty "za mordę", bo w końcu pewne propsy nie są wymagane.
Newsletter dla Frontend Developerów 📮
Kompozycja
Weźmy na tapet rozwiązanie zupełnie inne od pierwotnego. Skorzystajmy z mechanizmu children
i zamieńmy nasze propsy na reużywalne komponenty:
<Modal open={true} onClose={true}>
<Modal.Header>
<Modal.Title>...</Modal.Title>
<CloseIcon />
</Modal.Header>
<Modal.Description>...</Modal.Description>
<Modal.ActionsGroup>
<Modal.Action variant="red" onClick={() => {}}>
...
</Modal.Action>
<Modal.Action variant="red" onClick={() => {}}>
...
</Modal.Action>
</Modal.ActionsGroup>
</Modal>
Notacja z kropką jest całkowicie opcjonalna. Jest wykorzystywana często przez twórców bibliotek jako pewna konwencja nazewnicza.
Korzystamy tutaj z wyselekcjonowanych komponentów, które są skrojone pod nasz kawałek UI. Każdy puzzel takiej układanki może mieć własne propsy i children
.
Zalety
Elastyczność
Nie chcemy wyświetlać jakiegoś komponentu w danej sytuacji? Pyk, robimy warunek w
JSX
i po problemie.Chcemy zmienić kolejność wyświetlania poszczególnych elementów? Żaden problem!
Skalowalność
Potrzebujemy dodać nowy wariant przycisku, zmienić kolor tytułu, opisu itp. ? Nie ma problemu - dostosowujemy poszczególne komponenty.
Wady
Nieprzewidywalność
Elastyczność jest super, póki ktoś czegoś za bardzo nie namiesza. Co się stanie jeśli programista postanowi wrzucić randomowego diva w środek naszego komponentu? Czy stanie się coś złego, czy jesteśmy przygotowani na takie sytuacje?
Czy możemy jakoś temu zaradzić? Powiedzmy, że korzystamy z TypeScripta, dzięki któremu mamy możliwość otypować dokładnie nasze
children
... To by było zbyt piękne.Obecnie nie ma dobrego sposobu, że wskazać Reactowi, żeby przyjmował tylko określone dzieci. Co prawda możemy zastosować kilka "brudnych sztuczek", o których pisałem w artykule React Children & TypeScript - jak to ogarnąć?, ale czy jest to warte naszej pracy?
Elementy stałe.
Komponujemy poszczególne komponenty jeden po drugim w nadrzędnym komponencie
Modal
, wszystko jest cacy. Problem pojawia się, gdy w środkuModal
będziemy potrzebowali umieścić jakiś dodatkowy kod.Tytuł i opis mają być w jednym
div
, akcje w drugim itp. Rozwiązanie? Dodać więcej komponentów, które będziemy przekazywać wchildren
. Niestety wiążę się to z tworzeniem nadmiarowego kodu wJSX
.Czy mamy jeszcze jakieś opcje? Moglibyśmy pokusić się o sprawdzanie poszczególnych
children
, ale ponownie, warto zadać sobie pytanie, kiedy ma to sens. Nikt nie lubi nadmiarowego boilerplate'u.
Co wybrać?
Polecę klasykiem:
Nie ma złotego środka
Każde z tych rozwiązań ma swoje wady i zalety. Często mieszamy je ze sobą, w zależności od konkretnych przypadków.
Dużo zależy od skali komponentu. Inaczej możemy potraktować większy kawałek UI, a inaczej prosty przycisk.
Dostosuj podejście
W inny sposób będziesz budować bibliotekę open source, a w inny wewnętrzny zestaw komponentów.
Jeśli udostępniamy coś do szerszego grona odbiorców, warto postawić na bardziej elastyczne podejście. W teorii nasz komponent może działać świetnie, w praktyce mogą pojawić się nieoczekiwane przypadki brzegowe, gdzie sztywne podejście może być bardzo ograniczające. Z kompozycji korzystają popularne biblioteki UI, np. Radix, Headless UI, Chakra itp.
Z drugiej strony, jeśli pracujemy nad wewnętrznym rozwiązaniem, gdzie znamy wszystkie warianty komponentu, może warto rozważyć bardziej sztywne, dopasowane do naszych potrzeb rozwiązanie. Niekoniecznie musi być to w 100% sztywna konfiguracja.
Istotne jest to, jak zaczynamy
Naturalnym podejściem zdaje się rozpoczęcie od przekazania propsów, chleb powszedni React Developera. Jednak nie zawsze jest to dobre rozwiązanie.
Zaczynając od kompozycji możemy stworzyć niskopoziomowe API komponentu, a następnie nadbudować je sztywną konfiguracją. Niestety nie działa to w drugą stronę.
W obu podejściach musimy pamiętać o jednym - odpowiednim poziomie abstrakcji. Zasada Don't Repeat Yourself zakorzeniła się w głowach programistów na stałe. Czy słusznie? Przy tworzeniu komponentów warto mieć z tyłu głowy to, że duplikacja jest często sporo "tańsza" od niewłaściwej abstrakcji.
Do usłyszenia!