Home

React + Recompose = ❤

Recompose to jedna z moich ulubionych bibliotek do Reacta. Mimo, że React Hooks częściowo rozwiązuje problemy, z powodu których powstało Recompose, wciąż warto zapoznać się z tą biblioteką.

Recompose, pisze sam autor to zestaw HOC i utility do Reacta:

Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React.

Andrew Clark, autor

Utility rozwiązują często powtarzalne problemy, które poprawiają developer experience i tworzą abstrakcję dla często występujących problemów.

Wspomniane porównanie do Lodasha jest całkiem trafne. Na podobnej zasadzie działa Recompose, tyle że dla problemów które występują w Reakcie. Jako że operujemy tutaj na komponentach Reactowych, będziemy wyciągać z biblioteki Higher Order Componenty (a raczej ich fabryki), które mają konkretne zastosowania – np. state, mapowanie propsów, zarządzanie lifecycle i wiele innych. Na końcu możemy spiąć je ze sobą przy pomocy funkcji compose.

Warto wspomnieć, że Recompose był w szczególności potrzebny przed React Hooks. Wtedy część operacji była zarezerwowana dla komponentów opartych na klasach, na przykład state, lifecycle methods czy kontekst. Dzisiaj używając useState, useEffect czy React.memo(), funkcjonalności te są dostępne dla nas, używając komponentów funkcyjnych.

Czy to oznacza że nie warto używać Recompose? Otóż nic bardziej mylnego. Recompose przenosi pewne zachowania do innych warstw, dzięki czemu nasze komponenty przestają być wielkie i niezrozumiałe. Ponadto zachowują pełne SRP oraz są łatwe w testowaniu. Wadą Hooks jest właśnie zakrzywienie SRP komponentów, poprzez zahardkodowanie w nich efektów czy stanu. Oczywiście możemy stworzyć nasz własny HOC, który w środku ma hook i owinąć nim nasz komponent, ale Recompose robi de facto to samo (tylko w klasyczny sposób).

Przykłady

withProps

Załóżmy średniej wielkości komponent, posiadający kilka przycisków. Nasz komponent Button posiada cały szereg propsów, które można mu przekazać. Przykładowo:

export interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    primary?: boolean;
    secondary?: boolean;
    block?: boolean;
    iconLeft?: ReactNode;
    iconRight?: ReactNode;
    small?: boolean;
    superSmall?: boolean;
}

Co oznacza:

  1. <Button> implementuje <button>, czytaj przyjmuje wszystkiego jego atrybuty. Możesz przekazać mu np. type czy onClick. Nasz komponent owija ten natywny w dodatkowe zachowania, wygląd itd.
  2. <Button> przyjmuje szereg dodatkowych propsów, zarówno warianty (small, primary) jak i np komponenty do wyświetlenia (iconLeft, iconRight).

Wyobraź sobie teraz, że za każdym razem gdy chcemy wyświetlić określony rodzaj buttona w naszym rodzicu, musimy pisać to:

<Button 
  primary 
  small 
  block 
  iconLeft={<UserIcon />}
>
  Add user
</Button>

Dużo niepotrzebnych linii kodu, ale przecież nie chcemy rezygnować z możliwości dopasowania wariantów komponentów. W tym samym rodzicu możemy mieć kolejne buttony z innymi ikonami, ale mającymi te same inne propsy – np. save user, cancel.

Tutaj recompose dostarcza nam świetny HOC o nazwie withProps, używam go bardzo często.

Zobaczmy jak to wygląda:

import Button from './components/button';

const SomeButtonWithVariant = withProps({
  primary: true, 
  small: true,
  block: true,
})(Button);

const SomeParentComponent = (props) => (
  <div>
  ...
  <SomeButtonWithVariant 
    iconLeft={<UserIcon />}
  >
    Add user
  </SomeButtonWithVariant>

  ...

  <SomeButtonWithVariant 
    iconLeft={<SaveIcon />}
  >
    Save user
  </SomeButtonWithVariant>

  ...
  </div>
);

Jak widać, jesteśmy DRY, bo skomponowaliśmy sobie nowy, pośredni komponent z określonym zestawem cech.

Nic nie stoi na przeszkodzie, aby w taki sposób tworzyć bibliotekę komponentów, która przy określonych kompozycjach będzie bardzo łatwo rozszerzalna – SomeButtonWithVariant ma wszystkie właściwości buttona, ale równocześnie jest pełnoprawnym komponentem, za to Button może być tworzony na kolejne sposoby, łącząc różne zestawy propsów.

withState

Kolejny ciekawy HOC to withState, który oczywiście pozwala nam używać stanu, ale równocześnie bez używania hooka wewnątrz naszego czystego komponentu oraz bez tworzenia class componentu.

Posłużę się przykładem z dokumentacji:

const addCounting = compose(
  withState('counter', 'setCounter', 0),
  withHandlers({
    increment: ({ setCounter }) => () => setCounter(n => n + 1),
    decrement: ({ setCounter }) => () =>  setCounter(n => n - 1),
    reset: ({ setCounter }) => () => setCounter(0)
  })
);
  1. W pierwszej linii użyta jest funkcja compose, która działa dokładnie jak zagnieżdżenie funkcji w funkcji, tylko ładniej. Jest to coś w rodzaju pipe’a i to podstawa programowania funkcyjnego. Odpowiednikiem zapisu bez compose będzie const addCounting = withState(withHandlers(Component)), co traci na czytelności. Implementacja compose jest trywialna.
  2. W drugiej linii użyty jest helper withState, który jedynie przekazuje propsy stanu (coś jak reduxowy mapStateToProps). Tworzymy nazwę pola, która zostanie przekazana jako prop, nazwę settera który ustawi stan oraz inicjalną wartość. W ostatnim argumencie przekazujemy inicjalną wartość. Jak widać przypomina to Hook useState. Teraz, komponent otrzymałby dodatkowe propsy counter oraz setCounter, jednak lepiej stworzyć dodatkowe funkcje aktualizujące stan…
  3. … które są w trzeciej linii – HOCu useHandlers. Ten otrzymuje już „z góry” propsy (a używa jednego – setCounter) i tworzy trzy funkcje aktualizujące stan. Trochę jak reduxowy mapDispatchToProps.
  4. compose zwraca nam HOC, musimy więc na końcu owinąć nim komponent, np const CompWithCounting = addCounting(Component)

Zaletą takiego podejścia jest wyciągnięcie stanu i „logiki” z komponentu, który po prostu otrzyma „ścianę” propsów do przemielenia na widok i interakcje użytkownika.

Jest to też dużo czytelniejsze rozwiązanie, niż gdybyśmy mieli pisać HOC-i samemu.

Typescript

Bardzo ważne dla mnie jest wsparcie (community, ale jednak) dla TS. Każdy helper możemy mapować na podstawie jego propsów wejścia i wyjścia, dzięki czemu mamy kontrolę nad przepływem danych, a na samym końcu możemy zadeklarować jakie propsy jeszcze oczekujemy by zostały wprowadzone (już w JSX).

Inne ciekawe helpery

Zapobieganie rerenderom

Przykład z dokumentacji:

const HyperOptimizedComponent = onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent)

Obecnie jest to podobne do React.memo(), jednak mamy ładniejsze api 🙂 Oznacza to, że komponent zostanie przerenderowany tylko przy zmianie propA i propB.

Branch

Branching to coś w rodzaju if-else a dokładniej ternary operatora (x ? y : z). Recompose pozwala funkcyjnie przekazać jedną lub drugą funkcję, zależnie od warunku:

branch(
  test: (props: Object) => boolean,
  left: HigherOrderComponent,
  right: ?HigherOrderComponent
): HigherOrderComponent

Co oznacza, ze jeśli funkcja podana w pierwszym argumencie (która ma dostęp do obiektu props przekazanego z poprzednich funkcji) zwróci true, to zaaplikowany zostanie HOC przekazany w drugim argumencie, a jeśli false – ten w trzecim.

Przykładowo użyjemy go by owinąć komponent w ładowanie (przykład z dokumentacji):

const spinnerWhileLoading = isLoading =>
  branch(
    isLoading,
    renderComponent(Spinner)
  );

const enhance = spinnerWhileLoading(
  props => !(props.title && props.author && props.content)
)

Dzięki temu nie musimy nigdzie rozbijać komponentów i robić ifów by ręcznie pokazywać ładowanie – możemy skomponować sobie nasz custom HOC w ten sposób i reużywać go w wielu miejscach. W tym przypadku użyta została też funkcja renderComponent(Spinner), która po prostu renderuje komponent. Gdy branch nie otrzyma trzeciego argumentu, a isLoading będzie miało wartość false, zostanie wyrenderowany właściwy komponent.

Podsumowanie

Recompose posiada całą masę ciekawych funkcji, mimo że nie są dodawane już nowe, wciąż polecam się zapoznać z tą biblioteką. Zajmuje mało, a zwiększa czytelność i reużywalność kodu, sprawiając że nasze komponenty UI mają mniejszą odpowiedzialność.

Recompose też świetnie spina się z Formik’owym withFormik, o którym pisałem tutaj.

Pisząc ten artykuł znalazłem też implementację z użyciem Hooks, z którą też się zapoznam i postaram o niej napisać.