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ą:
- Kruchość (Fragility): Zmiany w implementacji komponentu nadrzędnego mogą nieoczekiwanie zepsuć komponenty podrzędne.
- Skomplikowany przepływ propsów: Trudności w przekazywaniu i zarządzaniu propsami w hierarchii dziedziczenia.
- Trudności w reużyciu logiki: Dziedziczenie często prowadzi do głębokich i sztywnych hierarchii, utrudniając elastyczne reużycie kodu w różnych kontekstach.
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?
- Elastyczność: Kompozycja pozwala na łatwiejsze łączenie i modyfikowanie funkcjonalności poprzez składanie małych, niezależnych komponentów.
- Jawność: Przepływ danych (propsów) jest bardziej jawny i łatwiejszy do śledzenia niż ukryte zależności w hierarchii dziedziczenia.
- Reużywalność: Małe, wyspecjalizowane komponenty są łatwiejsze do reużycia w różnych częściach aplikacji.
- Zgodność z filozofią Reacta: React promuje budowanie UI jako drzewa komponentów, gdzie każdy komponent zarządza swoim stanem i wyglądem, komunikując się z innymi przez propsy. Kompozycja idealnie wpisuje się w ten model.
Ć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:
- Stwórz komponent
Card
. - Zdefiniuj dla niego propsy:
title
(opcjonalny, na nagłówek),footer
(opcjonalny, na stopkę) ichildren
(na główną treść). - Wewnątrz
Card
, wyrenderuj strukturę karty (np. div z ramką). - Warunkowo renderuj nagłówek (np.
<div className="card-header">{props.title}</div>
), jeśli proptitle
został przekazany. - Wyrenderuj główną treść karty (
<div className="card-body">{props.children}</div>
). - Warunkowo renderuj stopkę (np.
<div className="card-footer">{props.footer}</div>
), jeśli propfooter
został przekazany. - W komponencie
App
użyj komponentuCard
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ą.
- Karta tylko z treścią (
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ą.