Container Component jest jednym ze stylów pisania komponentów w React, który pozwala na zachowanie pewnego porządku w kodzie.

Podczas czytania artykułu może pojawić się w głowie znak zapytania… Przecież przedstawiony przykład to podstawy Reacta – budowania komponentów stanowych oraz komponentów bezstanowych, odpowiadających za wyświetlanie danych.
Jeżeli przyjrzymy się temu wzorcowi, może się okazać, że za tym wszystkim może stać nieco szersza idea, którą postaram się przedstawić. Dodatkowo stosowanie takiego wzorca i przestrzeganie zasad, zwiększa porządek w aplikacji.

Oto przykład podstawowego Container i Stateless Component:

import React from 'react';

const BooksWidget = ({books}) => {
    return (
        <div>
            {books.map((book) => (book))}
        </div>
    );
}

class BooksContainer extends React.Component {

    /**
     * In real life - initial state = null, get data from API etc.
     */
    state = {
        books: ['Book 1', 'Book 2', 'Book 3']
    }

    getBooks() {
        // get data / setState etc.
    }

    render() {
        return (
            <BooksWidget books={this.state.books}/>
        )
    }
}

export default BooksContainer;

Jak widać na powyższym przykładzie, kontener odpowiada za warstwę danych, zaś widget za prezentację danych. Przestrzegamy też zasady Single Responsibility.

Idealnie byłoby też, gdyby kontener nie przyjmował żadnych propsów. Będzie on wtedy całkowicie niezależny, będzie można łatwiej go zrefaktorować, przenieść, zdebugować itd…
Czyli w skrócie, komponent powinien być wywoływany tak:
<BooksContainer />

Jeżeli nie przyjmowałby żadnych wartości z zewnątrz, byłoby super… Oczywiście wszystko zależy od złożoności aplikacji… Ważne, aby mieć na uwadze, aby przekazanych propsów, od których zależy container component, było jak najmniej.

Dodatkowo, kontener nie powinien mieć nic wspólnego z prezentacją danych, czyli nie powinien zawierać żadnego stylowania, CSSów, nie powinien korzystać z żadnych bibliotek do stylowania itp. Stylowanie pozostawiamy komponentom odpowiedzialnym za wyświetlanie danych. To daje nam kolejny podział warstw i odpowiedzialności.

Jakie jeszcze benefity otrzymujemy korzystając z takiego wzorca i trzymania się zasad?

Jeżeli przyjmiemy sztywną zasadę, że kontenery zawsze będą odpowiadały za warstwę danych, mamy już pewien porządek.

Gdy <BooksWidget /> otrzymuje dane, a nie sam odpowiada za ich pobieranie, możemy teraz wykorzystać ten widget do używania go w innych miejscach.

Na przykład w ramach sklepu internetowego – księgarni, możemy wyświetlić książki:

  • na liście kategorii,
  • na podstronie z listą promocji,
  • na podglądzie produktu w sekcji „te pozycję również mogą Ciebie zainteresować…”

Czyli w takim przypadku możemy mieć kontenery:

  • pierwszy – będzie odpowiedzialny za pobieranie i przekazanie danych do widgetu z informacją o książkach z danej kategorii,
  • drugi, będzie wyświetlał informacje o promocjach, warunkach promocji oraz dodatkowo pobierał dane dotyczące promocji (listę książek) i przekazywał je do tego samego widgetu,
  • trzeci kontener, będzie prezentował informacje o danej pozycji, ale również dostarczał dane do sekcji z proponowanymi innymi pozycjami, które mogą Ciebie zainteresować.

We wszystkich kontenerach korzystamy z tego samego <BooksWidget />

Jeżeli nasz komponent byłby odpowiedzialny za kilka czynności, czyli pobieranie i wyświetlanie danych, musielibyśmy kopiować komponent i zapewne powielać logikę, zmieniając np. adres URL, z którego pobieramy dane. Dodatkowo, przy zmianie wyglądu listy książek, czyli zmianie warstwy prezentacji, musielibyśmy zmieniać nasz kod w kilku komponentach.

Kolejnym plusem jest łatwiejsze testowanie tak zbudowanych komponentów, ponieważ komponent prezentacyjny nie wie nic o źródle danych, w jaki sposób te dane są pobierane, z jakiego źródła pochodzą itd. On zawsze otrzymuje je z zewnątrz.

Kontener oraz widgety, które otrzymują dane z kontenera
Kontener oraz komponenty zasilane danymi przez kontener.

Aha… Na koniec warto też zadbać o strukturę folderów, np. kontenery możemy przechowywać w folderze containers, a komponenty w folderze o nazwie components. Szkół przygotowania struktury katalogów w aplikacji jest wiele i myślę, że będzie to temat na osobny wpis.

Na koniec warto też zastanowić się, jak wyglądałby komponent, gdybyśmy nie przygotowali podziału komponentów na dwie warstwy?

import React from 'react';

class Books extends React.Component {

    /**
     * In real life - initial state = null
     */
    state = {
        books: ['Book 1', 'Book 2', 'Book 3']
    }

    getBooks() {
        // get data / setState etc.
    }

    render() {
        return (
            <div>
                {this.state.books.map((book) => (book))}
            </div>
        )
    }
}

export default Books;

Jak widzisz, zmniejszamy ilość komponentów, natomiast łamiemy zasadę podziału i odpowiedzialności. W naszym przypadku jeden komponent odpowiada zarówno za warstwę pobierania danych, jak również prezentacji danych. W dodatku nie możemy używać ponownie fragmentu odpowiedzialnego za wyświetlanie książek.

Może nadal wydawać się nam, że przecież nie jest to takie straszne.

OK – tutaj mamy do czynienia z bardzo prostym przykładem, natomiast wyobraźmy sobie, że nasz komponent <Books /> pobiera i prezentuje informacje o książkach, promocjach, renderuje ostatnio przeglądane pozycje i wyświetla nowości książkowe. Dodatkowo każdy fragment informacji o książce posiada swoją warstwę prezentacji, znaczniki <div>, <span>,  klasy, style itd… Nasz komponent urośnie wtedy do kilkuset linii i na pewno trudno będzie się w nim odnaleźć.
Dlatego bardzo ważne jest przestrzeganie zasad już od samego początku budowania aplikacji i nie robienia wyjątków nawet dla takich prostych przykładów.

Komponenty i React hooks

Od czasu, kiedy w życie na dobre weszły React hooks (od wersji 16.8.0) istnieje również podejście, że komponenty stanowe budujemy jako komponenty funkcyjne wraz z hookiem useState. Artykuł nie ma na celu narzucania konieczności używania komponentów stanowych lub funkcyjnych, a jedynie pokazać podejście dotyczące podziału odpowiedzialności komponentów.

Repozytorium

Bardzo prosty przykład, ale może komuś się przyda… https://github.com/magicwebpl/react-container-component-pattern

Zadanie domowe

Na koniec możesz spróbować stworzyć kilka kontenerów z opisanej wyżej „księgarni”, używając tego samego widgetu w kilku kontenerach. Np. podstrona kategorii, podstrona promocji itd.
Stwórz również jeden kontener, w którym kilka razy wykorzystasz ten sam widget – lista książek, lista polecanych pozycji, lista promocji. Jako źródło danych możesz wykorzystać zwykłą tablicę.

Odnośniki zewnętrzne:

O container component i presentational component – artykuł Dana Abramova:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

Źródła:
Obraz: https://unsplash.com/photos/RJjY5Hpnifk