Halo, halo! Witam po dłuższej przerwie, spowodowanej… trochę lenistwem, a trochę innymi sprawami życiowymi. No nic to. Najważniejsze, że w końcu wracam do pisania! A tematem dzisiejszego posta są komponenty wyższego rzędu w React, czyli z angielska higher order components (a skracając: HOC). W wielkim skrócie jest to technika re-używania logiki komponentów, która wbrew pozorom jest dość powszechnie stosowana. Ale o tym w dalszej części tego wpisu.

Podstawowe założenia HOC

Generalnie możemy powiedzieć, że komponent wyższego rzędu w React jest funkcją, która przekształca jeden komponent w inny komponent. Przekształcenie to zwykle powoduje “ulepszenie” w jakiś sposób oryginalnego komponentu. Myślę, że więcej wywnioskujesz analizując przykłady - wyobraźmy sobie, że mamy taki oto komponent App:

import React from 'react';

const App = (props) => {
  return <p>Hello World!!</p>;
}

export default App;

Mogę teraz utworzyć komponent wyższego rzędu, który będzie “ulepszał” ten komponent (umieszczę go w pliku enhanceApp.js):

import React from 'react';

export default (Component) => {
  return (props) => (
    <div style= {{color: 'red'}}>
      <Component {...props} />
    </div>
  );
};

Jak widzisz, z modułu tego eksportuję funkcję przyjmującą oryginalny komponent jako parametr. Funkcja ta zwraca nowy komponent funkcyjny (oczywiście dla bardziej zaawansowanych przypadków mogę też zwracać komponent klasowy).

Ten nowy zwracany komponent nie robi nic innego tylko znacznie “ulepsza” oryginalny komponent poprzez owinięcie go elementem div, który nadaje mu o wiele fajniejszy styl. Zwróć uwagę, że do oryginalnego komponentu przekazuję też wszystkie właściwości obiektu props (używam ... czyli operatora “spread”)! W przeciwnym wypadku, użycie HOC powodowałoby utratę informacji przekazywanych z komponentu rodzica.

Zobacz teraz jak używać HOC - robię to w pliku z komponentem App:

import React from 'react';
import enhanceApp from './enhanceApp';

const App = (props) => {
  return (
    <p>Hello World!!</p>
  );
}

export default enhanceApp(App);

Widać tutaj, że użycie komponentu wyższego rzędu sprowadza się do wywołania funkcji i przekazania do niej oryginalnego komponentu. W ten sposób, z powyższego modułu eksportujemy nie oryginalny komponent App, a ten już ulepszony przez HOC enhanceApp;

I to w zasadzie cała filozofia jeśli chodzi o komponenty wyższego rzędu. Oczywiście komponenty zwracane przez HOC mogą być znacznie bardziej skomplikowane, wykorzystujące metody cyklu życia komponentu itd.. Wszystko zależy od potrzeb i danej sytuacji.

Studium przypadku - funkcja connect

Na tym co pokazałem powyżej mógłbym już w zasadzie zakończyć, ale pomyślałem, że jako bonus spróbujemy zrozumieć zasadę działania jednego z najczęściej stosowanych w praktyce HOC. Chodzi mianowicie o funkcję connect dostępną w pakiecie react-redux, która łączy “store” Reduxa z komponentem React. Myślę, że większość programistów pracujących w React, miała z nią styczność lub chociaż mniej więcej ją kojarzy…

Nie będę tutaj pokazywać jej rzeczywistej implementacji, która jest trochę bardziej skomplikowana - tę możesz sprawdzić sam tutaj. Zamiast tego spróbujemy napisać jej własną, uproszczoną wersję. Najpierw jednak przykład kodu, który będzie wykorzystywać naszą własną implementację funkcji connect:

import React from 'react';
import connect from './custom-connect';

const Connected = (props) => {
  return <p>{props.text}</p>;
}

const mapStateToProps = (state) => {
  return { ...state, text: 'Hello from fake state!' };
};

const mapDispatchToProps = (dispatch) => {
  return {}; // no actions defined yet
};

export default connect(mapStateToProps, mapDispatchToProps)(Connected);

Jeśli znasz trochę zasady programowania funkcyjnego to zauważysz, że zastosowano tutaj tzw. “curry’ing” czyli “funkcja zwracająca funkcję”. Do funkcji connect przekazujemy bowiem referencje do metod mapujących i dopiero do wyniku jej działania przekazujemy komponent Connected.

W powyższym przykładzie widać też, że funkcja mapStateToProps wzbogaca stan o właściwość text, z której korzysta komponent Connected. Aby jednak właściwość ta była dostępna dla komponentu, musimy zaimplementować naszą funkcję connect, która uruchomi funkcje mapujące i ich wyniki dorzuci do “propsów”. Zastanówmy się więc teraz, jak można by to zaimplementować (plik custom-connect.js):

import React from 'react';

// fake Redux stuff for the example purposes
const fakeState = {};
const fakeDispatch = () => {};

export default (mapStateToProps, mapDispatchToProps) => (Component) => {
  return (props) => {
    // the main purpose of this HOC - add Redux stuff to the Component's props
    const newProps = {
      ...props,
      ...mapStateToProps(fakeState),
      ...mapDispatchToProps(fakeDispatch)
    };

    return <Component {...newProps} />;
  };
};

Szybkie wyjaśnienie: tak jak wspomniałem wcześniej, funkcja connect wykorzystuje “curry’ing” stąd zapis w stylu:

const example = (arg1) => (arg2) => { ... } ;

Dla nie obeznanych z funkcjami strzałkowymi, jest to tożsame z zapisem:

const example = function (arg1) {
  return function (arg2) { ... }
}

No ale wracając do rzeczy… zauważ, że na początku deklaruję stałe fakeState oraz fakeDispatch - oczywiście w oryginale wykorzystywane są odpowiednie funkcje dostarczane przez Reduxa.

Dalej mamy już właściwą implementację funkcji connect (domyślny eksport), która przyjmuje jako parametry referencje do funkcji mapujących, a następnie zwraca funkcję, do której przekazywany jest oryginalny komponent (Component).

Ta funkcja z kolei, zwraca nowy komponent funkcyjny, w którym tworzony jest obiekt newProps. Korzystając z operatora “spread” kopiuję do niego wszystkie właściwości oryginalnych “propsów” oraz właściwości obiektów zwracanych przez funkcje mapujące.

Na koniec, ponownie korzystając operatora “spread”, przekazuję wszystkie właściwości obiektu newProps jako atrybuty wywołania komponentu Component. W ten sposób funkcja connect rozszerza “propsy” oryginalnego komponentu.

Podsumowanie

I to wszystko na dziś. Mam nadzieję, że to co pokazałem jest zrozumiałe - jeśli nie, śmiało pisz w komentarzach lub bezpośrednio do mnie, na e-mail. Wydaje mi się, że koncepcja komponentów wyższego rzędu jest bardzo przydatna - na przykład kiedy potrzebujemy mieć mechanizm, który rozszerza część naszych komponentów w określony sposób. Dlatego też warto ją znać!

P.S. Jak zawsze udostępniam na GitHubie kod przedstawionych dziś przykładów do samodzielnego przetestowania. Odpowiednie repozytorium znajdziesz pod tym linkiem.