Kategoria:React

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:

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.

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 środku Modal 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ć w children. Niestety wiążę się to z tworzeniem nadmiarowego kodu w JSX.

    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:

To zależy.
  1. 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.

  2. 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.

  3. 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ę.

    To zależy.

    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!

O autorze

Olaf Sulich

Olaf jest Frontend Developerem, blogerem i nosi rybacki kapelusz 🎩 Pisze o wszystkim co związane z frontendem, ale nie boi się backendu i designów 🦾 Ma głowę pełną pomysłów i nadzieję, że znajdziesz tutaj coś dla siebie!

Dołącz do społeczności!

Bo w programowaniu liczą się ludzie

Wchodzę