Logo of
Published on

Jak uniknąć przekazywania wielu propsów do komponentów za pomocą kompozycji komponentów w React

Authors
  • avatar
    Name
    Marcin Parda
    Twitter

Problem

Wyobraź sobie, że musisz zaimplementować w swojej aplikacji prosty panel, który wyświetli użytkownikowi informację o pomyślnie wykonanej akcji. Wystarczy, że przekażesz kilka propsów do komponentu odpowiedzialnego za wyświetlanie tego panelu. Wszystko wskazuje na to, że możesz to zrobić w 5 minut.

Zaczynasz nad tym pracować, ale kiedy zaczynasz implementować ten komponent, zdajesz sobie sprawę, że musisz przekazać kilka propsów związanych z tym, jak wygląda nagłówek panelu.

Następnie zauważasz, że musisz przekazać kilka propsów związanych z tym, jak wygląda treść w tym panelu.

Później zauważasz, że musisz przekazać propsy odpowiedzialne za to, jak wyglądają przyciski zwijania i zamykania panelu oraz czy w ogóle je pokazywać.

I tak dalej.

W rezultacie nasz "mały" potwór wygląda tak:

<Panel
  headerTitle="Panel title"
  headerIcon="icon"
  headerIconColor="red"
  headerTextColor="white"
  headerButtonCloseColor="white"
  headerButtonCloseVisible={true}
  headerButtonCloseOnClick={() => {}}
  headerButtonCollapseColor="white"
  headerButtonCollapseVisible={true}
  headerButtonCollapseOnClick={() => {}}
  contentBackgroundColor="white"
  contentTextColor="black"
  contentText="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eget ultricies ultricies, nunc
  tortor aliquam nisl, eget ultricies lorem ipsum vitae nunc. Donec auctor, nisl eget ultricies ultricies, nunc tortor aliquam
  nisl, eget ultricies lorem ipsum vitae nunc."
  contentButtonColor="blue"
  ...
/>

Gdy już zaimplementujesz swój komponent, czas przejść do następnego komponentu do zaimplementowania, którym będzie panel wyświetlający użytkownikowi błąd akcji. W tym momencie masz dwie opcje:

  1. Zaimplementuj nowy komponent tak samo jak poprzedni, ale o nieco innym wyglądzie i nazwie.
  2. Dodaj propsy do komponentu Panel, które będą odpowiedzialne za wyświetlanie błędu. W rezultacie liczba propsów może się nawet podwoić :(

...albo możesz dowiedzieć się o kompozycji komponentów i compound components i zaimplementować swój komponent w taki sposób, aby nie musieć przekazywać do niego dziesiątek propsów. W tym artykule omówimy pierwszą opcję.

Rozwiązanie

Kompozycja komponentów

Zacznijmy od techniki kompozycji komponentów. Jest to technika, która pozwala tworzyć bardziej złożone komponenty poprzez komponowanie mniejszych, logicznych podkomponentów.

Możesz znać tag HTML <table>. Wewnątrz tego tagu możesz umieścić tagi <tr> i <td>, które są odpowiedzialne za wyświetlanie wierszy i kolumn w tabeli.

Wszystkie te tagi są ze sobą powiązane, a ich wspólnym rodzicem jest tag <table>. W ten sposób możesz tworzyć bardziej złożone komponenty, komponując mniejsze, logiczne podkomponenty.

W podobny sposób możesz tworzyć bardziej złożone komponenty w React, komponując mniejsze, logiczne podkomponenty.

Jeśli korzystałeś z biblioteki Material UI, musisz znać komponenty takie jak Button, TextField lub Select.

Ich użycie może wyglądać tak:

<Select
  labelId="demo-simple-select-label"
  id="demo-simple-select"
  value={age}
  label="Age"
  onChange={handleChange}
>
  <MenuItem value={10}>Ten</MenuItem>
  <MenuItem value={20}>Twenty</MenuItem>
  <MenuItem value={30}>Thirty</MenuItem>
</Select>

Jak widać w powyższym przykładzie, Select przyjmuje swoje dzieci w postaci kilku MenuItem, zamiast przekazywać te elementy w propsach.

Dzięki temu elementy te można modyfikować niezależnie od komponentu Select. Ale jak możemy zaimplementować nasz Panel w taki sposób?

Implementacja kompozycji komponentów

  1. Najpierw zdefiniujmy interfejs dla naszego (nieco uproszczonego) komponentu Panel:
// Panel.tsx
interface PanelProps {
  title: string
}

// types.ts
interface ParentProps {
  children: React.ReactNode
}
  1. Następnie zdefiniujmy komponent Panel:
// Panel.tsx
export const Panel = ({ title, children }: PanelProps & ParentProps) => {
  return (
    <div>
      <h1>{title}</h1>
      {children}
    </div>
  )
}
  1. Teraz zdefiniujmy komponenty PanelHeader i PanelBody:
// PanelHeader.tsx
export const PanelHeader = ({ children }: ParentProps) => {
  return <header>{children}</header>
}

// PanelBody.tsx
export const PanelBody = ({ children }: ParentProps) => {
  return <main>{children}</main>
}
  1. Całość będzie wyglądać tak:

Panel.tsx

import { ParentProps } from './types'

interface PanelProps {
  title: string
}

export const Panel = ({ title, children }: PanelProps & ParentProps) => {
  return (
    <div>
      <h1>{title}</h1>
      {children}
    </div>
  )
}

PanelHeader.tsx

import { ParentProps } from './types'
export const PanelHeader = ({ children }: ParentProps) => {
  return <header>{children}</header>
}

PanelBody.tsx

import { ParentProps } from './types'
export const PanelBody = ({ children }: ParentProps) => {
  return <main>{children}</main>
}

Dzięki temu możemy używać tych komponentów w następujący sposób:

<Panel title="Panel">
  <PanelHeader>
    <h3>Header</h3>
  </PanelHeader>
  <PanelBody>
    <p>Body</p>
  </PanelBody>
</Panel>

Dzięki temu osiągnęliśmy kilka rzeczy:

  • PanelHeader i PanelBody są komponentami potomnymi Panel, możemy je łatwo modyfikować bez modyfikowania Panel, unikając props drilling.
  • Usunęliśmy z Panel odpowiedzialność za renderowanie PanelHeader i PanelBody. Gdyby którykolwiek z tych komponentów miał swój własny stan, renderowanie go nie spowodowałoby ponownego renderowania dużego komponentu Panel.
  • Łatwo uzyskaliśmy możliwość modyfikacji komponentu Panel. Nic nie stoi na przeszkodzie, aby zamienić PanelHeader i PanelBody w miejscu lub usunąć jeden z nich.

Ale dodaliśmy też kilka problemów:

  • Dodaliśmy nową warstwę abstrakcji, o której musisz pamiętać i wiedzieć, że istnieje. Przydałoby się też ją udokumentować.
  • Nie wiemy dokładnie, jakie komponenty powinniśmy użyć w Panel, jeśli chcemy, aby cały komponent wyglądał spójnie i działał poprawnie. Przydałoby się mieć rodzaj podpowiedzi składni, która powiedziałaby nam, co powinniśmy użyć w Panel, prawda? :)
  • Gdybyśmy chcieli, aby komponenty mogły ze sobą komunikować się, np. gdy klikniemy przycisk w PanelHeader chcielibyśmy ukryć cały komponent Panel, to musielibyśmy przekazać dodatkowe propsy do komponentów (handleClick, isPanelVisible).

Rozwiązaniem ostatnich dwóch (a częściowo pierwszego) problemów jest użycie compound components z Context API.

W następnym artykule opiszę, jak to zrobić.

Podsumowanie

To wszystko. Mam nadzieję, że podobał Ci się ten artykuł. Jeśli masz jakieś pytania, śmiało zadaj je w komentarzach.

Oto dodatek dla Ciebie. Krótkie podsumowanie artykułu. Możesz go wykorzystać do tworzenia kart fiches (np. w Anki).

Czym jest kompozycja komponentów?

Kompozycja komponentów to technika, która pozwala tworzyć bardziej złożone komponenty poprzez komponowanie mniejszych, logicznych podkomponentów.

Przykład:

<Panel title="Panel">
  <PanelHeader>
    <h3>Header</h3>
  </PanelHeader>
  <PanelBody>
    <p>Body</p>
  </PanelBody>
</Panel>

Widzisz błąd / literówkę w artykule? Zgłoś poprawkę lub dodaj komentarz na dole.

Zobacz post na GitHubie