Kompozycja a dziedziczenie

React posiada potężny model kompozycyjny, z którego zalecamy korzystać zamiast dziedziczenia, aby komponentów można było używać wielokrotnie.

W tej sekcji rozważymy kilka problemów, przy okazji których początkujący użytkownicy Reacta sięgają po dziedziczenie, a następnie pokażemy, jak rozwiązać je za pomocą kompozycji.

Zawieranie

Niektóre komponenty nie wiedzą z góry, co będzie ich “dziećmi”. Najczęściej dotyczy to komponentów takich jak Sidebar czy Dialog, które reprezentują “pojemniki” ogólnego użytku.

Zalecamy, aby komponenty tego typu korzystały ze specjalnego atrybutu children i przekazywały go bezpośrednio do renderowanej struktury:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

Pozwala to innym komponentom przekazywać dowolnych potomków poprzez zagnieżdżanie elementów JSX-owych:

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Witaj
      </h1>
      <p className="Dialog-message">
        Dziękujemy za wizytę na naszym statku kosmicznym!
      </p>
    </FancyBorder>
  );
}

Przetestuj kod na CodePen

Wszystko, co znajdzie się w JSX-owym znaczniku <FancyBorder> zostanie przekazane do komponentu FancyBorder poprzez atrybut children. Jako że FancyBorder renderuje {props.children} wewnątrz elementu <div>, właśnie w takim elemencie pojawią się ostatecznie przekazane komponenty.

Mimo że zdarza się to rzadziej, czasami trzeba wstawić do komponentu wiele takich “dziur”. W takich przypadkach można wymyślić własną konwencję i używać jej zamiast children:

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

Przetestuj kod na CodePen

Elementy reactowe, takie jak <Contacts /> czy <Chat />, są zwykłymi obiektami, dlatego możesz przekazywać je poprzez atrybuty jak każdą inną wartość. To podejście może przypominać koncepcję “slotów” z innych bibliotek, lecz w Reakcie nie ma żadnych ograniczeń co do typu wartości przekazywanych w atrybutach.

Specjalizacja

Czasami wyobrażamy sobie, że niektóre komponenty są “specjalnymi przypadkami użycia” innych komponentów. Na przykład, można by powiedzieć, że WelcomeDialog jest specjalnym przypadkiem komponentu Dialog.

W Reakcie taką relację również można osiągnąć poprzez kompozycję, gdzie “wyspecjalizowany” komponent renderuje inny, bardziej ogólny komponent i konfiguruje go za pomocą odpowiednich atrybutów:

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Witaj"
      message="Dziękujemy za wizytę na naszym statku kosmicznym!" />
  );
}

Przetestuj kod na CodePen

Kompozycja działa równie dobrze z komponentami klasowymi:

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Program Eksploracji Marsa"
              message="Jak powinniśmy się do Ciebie zwracać?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Zapisz mnie!
        </button>
      </Dialog>
    );
  }

  handleChange(e) {
    this.setState({login: e.target.value});
  }

  handleSignUp() {
    alert(`Witaj na pokładzie, ${this.state.login}!`);
  }
}

Przetestuj kod na CodePen

A co z dziedziczeniem?

W Facebooku korzystamy z Reacta w tysiącach komponentów i nie znaleźliśmy jak dotąd żadnego przypadku użycia, w którym lepszym rozwiązaniem byłoby stworzenie hierarchii dziedziczenia.

Atrybuty i kompozycja dają wystarczającą dowolność w dostosowaniu zarówno wyglądu, jak i zachowania komponentu, w sposób jawny i bezpieczny. Pamiętaj, że komponenty mogą przyjmować atrybuty dowolnego rodzaju: typy podstawowe, elementy reactowe czy funkcje.

Jeśli planujesz wielokrotnie używać w różnych komponentach funkcjonalności niezwiązanej z renderowaniem, sugerujemy wydzielić ją do osobnego modułu javascriptowego. Wtedy komponenty będą mogły ją zaimportować bez rozszerzania, bez względu na to, czy to funkcja, obiekt czy klasa.