Lekcja 12: Kompozycja vs Dziedziczenie

W programowaniu obiektowym dziedziczenie jest jednym z podstawowych mechanizmów reużywania kodu. Jednak w React, zamiast dziedziczenia, preferowanym i znacznie częściej stosowanym podejściem do budowania reużywalnych komponentów jest kompozycja.

Dziedziczenie w Kontekście Reacta

Dziedziczenie polega na tworzeniu nowej klasy (klasy podrzędnej), która przejmuje właściwości i metody innej klasy (klasy nadrzędnej). W kontekście komponentów klasowych React (choć obecnie rzadziej używanych), teoretycznie można by tworzyć hierarchie dziedziczenia komponentów.

// Teoretyczny przykład (NIEZALECANY w React)
class BaseButton extends React.Component {
  render() {
    return <button className="base-button">{this.props.children}</button>;
  }
}

class DangerButton extends BaseButton {
  render() {
    // Próba rozszerzenia lub modyfikacji renderowania rodzica
    const baseRender = super.render(); // To nie działa tak prosto w React
    // ... modyfikacje ...
    // W praktyce musielibyśmy skopiować i zmodyfikować logikę renderowania,
    // co łamie zasadę reużywalności.
    return <button className="base-button danger-button">{this.props.children}</button>;
  }
}

Zespół Reacta generalnie odradza stosowanie dziedziczenia między komponentami. Problemy z dziedziczeniem w React obejmują:

Kompozycja w React

Kompozycja polega na budowaniu złożonych komponentów poprzez składanie prostszych, niezależnych komponentów. React został zaprojektowany z myślą o kompozycji. Istnieją dwa główne sposoby jej wykorzystania:

1. Zawieranie (Containment) - Użycie `props.children`

Komponenty mogą przyjmować inne komponenty lub elementy JSX jako swoje dzieci (props.children). Pozwala to tworzyć generyczne "pudełka" lub "kontenery", które nie wiedzą z góry, co będą zawierać.

// Generyczny komponent ramki
function FancyBorder(props) {
  return (
    <div className={\'FancyBorder FancyBorder-\' + props.color}>
      {props.children} {/* Renderuje wszystko, co jest w środku */}
    </div>
  );
}

// Użycie kompozycji z zawieraniem
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      {/* Te elementy są przekazywane jako props.children do FancyBorder */}
      <h1 className="Dialog-title">
        Witaj!
      </h1>
      <p className="Dialog-message">
        Dziękujemy za odwiedzenie naszej przestrzeni kosmicznej!
      </p>
    </FancyBorder>
  );
}

Komponent FancyBorder działa jak opakowanie, dodając ramkę do dowolnej zawartości przekazanej jako children.

2. Specjalizacja - Przekazywanie Komponentów jako Props

Czasami chcemy, aby komponent-kontener miał bardziej specyficzne "dziury" do wypełnienia niż tylko ogólne children. Możemy zdefiniować własne propsy, które przyjmują komponenty lub elementy JSX.

// Komponent Dialog, który ma dedykowane miejsca na tytuł, treść i akcje
function Dialog(props) {
  return (
    <FancyBorder color="red">
      <div className="Dialog">
        <header className="Dialog-header">
          {props.title} {/* Miejsce na tytuł */}
        </header>
        <main className="Dialog-body">
          {props.message} {/* Miejsce na treść */}
        </main>
        <footer className="Dialog-footer">
          {props.actions} {/* Miejsce na przyciski akcji */}
        </footer>
      </div>
    </FancyBorder>
  );
}

// Użycie specjalizacji
function ConfirmationDialog() {
  const handleConfirm = () => { console.log("Potwierdzono!"); };
  const handleCancel = () => { console.log("Anulowano."); };

  return (
    <Dialog
      title={<h1 style={{color:"orange"}}>Potwierdzenie</h1>} // Można przekazać JSX
      message="Czy na pewno chcesz wykonać tę akcję?"
      actions={(
        <> // Fragment do grupowania przycisków
          <button onClick={handleConfirm}>Potwierdź</button>
          <button onClick={handleCancel}>Anuluj</button>
        </>
      )}
    />
  );
}

W tym przykładzie Dialog jest bardziej wyspecjalizowanym kontenerem niż FancyBorder. Definiuje on konkretne propsy (title, message, actions), które mogą być wypełnione przez komponent nadrzędny (ConfirmationDialog) dowolną zawartością JSX.

Komponent ConfirmationDialog jest specjalizacją komponentu Dialog, ale osiągniętą przez kompozycję (przekazanie odpowiednich propsów), a nie przez dziedziczenie.

Dlaczego Kompozycja jest Lepsza w React?


Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/Card.jsx
import React from \'react\';

function Card({ title, footer, children }) {
  const cardStyle = {
    border: \'1px solid #ddd\',
    borderRadius: \'8px\',
    margin: \'10px\',
    padding: \'0\',
    overflow: \'hidden\',
    boxShadow: \'0 2px 4px rgba(0,0,0,0.1)\'
  };
  const headerStyle = { background: \'#f7f7f7\', padding: \'10px 15px\', borderBottom: \'1px solid #ddd\' };
  const bodyStyle = { padding: \'15px\' };
  const footerStyle = { background: \'#f7f7f7\', padding: \'10px 15px\', borderTop: \'1px solid #ddd\' };

  return (
    <div style={cardStyle}>
      {title && (
        <div style={headerStyle}>
          {typeof title === \'string\' ? <h3 style={{ margin: 0 }}>{title}</h3> : title}
        </div>
      )}
      <div style={bodyStyle}>
        {children}
      </div>
      {footer && (
        <div style={footerStyle}>
          {footer}
        </div>
      )}
    </div>
  );
}

export default Card;

// src/App.jsx
import React from \'react\';
import Card from \'./Card\';

function App() {
  return (
    <div>
      <h1>Przykłady Kart</h1>

      <Card>
        <p>To jest prosta karta tylko z treścią.</p>
      </Card>

      <Card title="Karta z Tytułem">
        <p>Ta karta ma tytuł i treść.</p>
        <p>Można tu umieścić więcej informacji.</p>
      </Card>

      <Card footer={<button>Akcja</button>}>
        <p>Ta karta ma treść i stopkę z przyciskiem.</p>
      </Card>

      <Card 
        title={<em>Karta Pełna</em>} 
        footer={(
          <div style={{ display: \'flex\', justifyContent: \'space-between\' }}>
            <button>OK</button>
            <button>Anuluj</button>
          </div>
        )}
      >
        <p>Ta karta wykorzystuje wszystkie możliwości: tytuł (jako JSX), treść i stopkę (jako JSX).</p>
      </Card>
    </div>
  );
}

export default App;

Cel: Stworzyć komponent Card, który może być używany do wyświetlania różnych treści z opcjonalnym nagłówkiem i stopką, wykorzystując kompozycję.

Kroki:

  1. Stwórz komponent Card.
  2. Zdefiniuj dla niego propsy: title (opcjonalny, na nagłówek), footer (opcjonalny, na stopkę) i children (na główną treść).
  3. Wewnątrz Card, wyrenderuj strukturę karty (np. div z ramką).
  4. Warunkowo renderuj nagłówek (np. <div className="card-header">{props.title}</div>), jeśli prop title został przekazany.
  5. Wyrenderuj główną treść karty (<div className="card-body">{props.children}</div>).
  6. Warunkowo renderuj stopkę (np. <div className="card-footer">{props.footer}</div>), jeśli prop footer został przekazany.
  7. W komponencie App użyj komponentu Card na kilka sposobów:
    • Karta tylko z treścią (children).
    • Karta z tytułem i treścią.
    • Karta z treścią i stopką (np. przyciskami).
    • Karta z tytułem, treścią i stopką.

Zadanie do samodzielnego wykonania

Stwórz komponent SidebarLayout, który przyjmuje dwa propsy: sidebarContent i mainContent. Komponent powinien renderować układ strony z panelem bocznym po lewej stronie (np. o stałej szerokości) i główną treścią po prawej. Użyj tego komponentu w App.jsx, przekazując do niego różne komponenty lub elementy JSX jako zawartość panelu bocznego i głównej części strony.


FAQ - Kompozycja vs Dziedziczenie

Czy dziedziczenie jest całkowicie zabronione w React?

Nie jest technicznie zabronione, ale jest silnie odradzane jako sposób na reużywanie kodu *między komponentami*. Dziedziczenie jest nadal używane w React, np. komponenty klasowe dziedziczą po `React.Component` lub `React.PureComponent`. Jednak tworzenie własnych hierarchii dziedziczenia komponentów UI jest uważane za anty-wzorzec.

Jakie są główne techniki kompozycji w React?

Dwie główne techniki to: 1) **Zawieranie (Containment):** Użycie `props.children` do tworzenia generycznych kontenerów, które mogą opakowywać dowolną zawartość. 2) **Specjalizacja:** Definiowanie specyficznych propsów, które przyjmują komponenty lub JSX, pozwalając na tworzenie bardziej strukturalnych szablonów z dedykowanymi "dziurami" do wypełnienia.

Czy mogę przekazać wiele elementów jako `props.children`?

Tak, `props.children` może być pojedynczym elementem, tablicą elementów, stringiem, lub nawet `undefined`, jeśli nic nie zostanie przekazane. React potrafi obsłużyć wszystkie te przypadki. Jeśli przekazujesz wiele elementów, `props.children` będzie tablicą.

Jak kompozycja pomaga w unikaniu "prop drilling"?

Sama kompozycja nie rozwiązuje bezpośrednio problemu "prop drilling" (przekazywania propsów przez wiele poziomów). Jednak, stosując kompozycję, możemy często przekazać bardziej złożone fragmenty UI (zawierające potrzebne dane lub logikę) jako `children` lub inne propsy, zamiast przekazywać pojedyncze dane w dół drzewa. Dla globalnego stanu, lepszym rozwiązaniem jest Context API lub biblioteki zarządzania stanem.

Czy komponenty funkcyjne mogą używać kompozycji?

Oczywiście! Kompozycja jest podstawowym mechanizmem w React i działa tak samo dobrze (a nawet naturalniej) z komponentami funkcyjnymi, jak z klasowymi. Przykłady w tej lekcji używają komponentów funkcyjnych.

Czy istnieją przypadki, gdzie dziedziczenie mogłoby być użyteczne w React?

Bardzo rzadko w kontekście komponentów UI. Czasami dziedziczenie może być użyteczne do współdzielenia logiki *poza* renderowaniem, np. w pomocniczych klasach JavaScript używanych przez komponenty, ale nie do tworzenia hierarchii samych komponentów.

Jak kompozycja ma się do Hooków?

Hooki (zwłaszcza niestandardowe Hooki - custom hooks) są innym potężnym mechanizmem reużywania logiki (w tym logiki stanowej i efektów ubocznych) w komponentach funkcyjnych. Kompozycja dotyczy głównie struktury UI i składania komponentów, podczas gdy Hooki dotyczą reużywania zachowań i logiki. Te dwa mechanizmy doskonale się uzupełniają.