Kurs JavaScript i React · Moduł 2: Komponenty i props

Kompozycja komponentów

6 min czytania
W tej lekcji8

Stacja kosmiczna nie powstaje z jednego kawałka metalu - składa się z modułów, które są dokowane do siebie w przemyślany sposób. Moduł laboratoryjny łączy się z modułem mieszkalnym, a ten z modułem energetycznym. Każdy z nich może funkcjonować samodzielnie, ale dopiero razem tworzą funkcjonalną całość.

W React kompozycja komponentów działa na identycznej zasadzie. Zamiast budować jeden ogromny komponent, tworzymy mniejsze, wyspecjalizowane elementy i łączymy je ze sobą. To jeden z najważniejszych wzorców projektowych w React i klucz do tworzenia skalowalnych aplikacji.

Children - zawartość wewnętrzna komponentu

Najprostszą formą kompozycji jest użycie specjalnego propsa children. Każdy komponent React może przyjmować elementy zagnieżdżone między znacznikami otwierającym i zamykającym:

1function SpaceModule({ title, children }) {
2  return (
3    <div className="space-module">
4      <div className="module-header">
5        <h3>{title}</h3>
6        <span className="status-light" />
7      </div>
8      <div className="module-content">
9        {children}
10      </div>
11    </div>
12  );
13}
14
15// Użycie - "dokujemy" dowolną zawartość do modułu
16function SpaceStation() {
17  return (
18    <div className="station">
19      <SpaceModule title="Laboratorium">
20        <p>Badania mikrograwitacji</p>
21        <p>Eksperymenty biologiczne</p>
22      </SpaceModule>
23
24      <SpaceModule title="Centrum dowodzenia">
25        <button>Nawigacja</button>
26        <button>Komunikacja</button>
27        <button>Systemy podtrzymywania życia</button>
28      </SpaceModule>
29    </div>
30  );
31}

Prop children pozwala tworzyć komponenty-kontenery, które nie wiedzą z góry, co będą wyświetlać. To jak budowanie uniwersalnych portów dokowania - mogą przyjąć dowolny moduł.

Wzorzec slotów - wiele punktów kompozycji

Czasami jeden children nie wystarczy. Potrzebujemy kilku "miejsc" w komponencie, gdzie możemy wstawić różne treści. W React realizujemy to przez przekazywanie komponentów jako zwykłych propsów:

1function MissionPanel({ header, sidebar, content, footer }) {
2  return (
3    <div className="mission-panel">
4      <div className="panel-header">{header}</div>
5      <div className="panel-body">
6        <aside className="panel-sidebar">{sidebar}</aside>
7        <main className="panel-content">{content}</main>
8      </div>
9      <div className="panel-footer">{footer}</div>
10    </div>
11  );
12}
13
14// Każdy "slot" otrzymuje inny komponent
15function MissionDashboard() {
16  return (
17    <MissionPanel
18      header={<h1>Misja Apollo</h1>}
19      sidebar={<NavigationMenu />}
20      content={<MissionDetails missionId="apollo-11" />}
21      footer={<StatusBar status="active" />}
22    />
23  );
24}

Ten wzorzec daje ogromną elastyczność - komponent MissionPanel definiuje układ, ale nie narzuca treści.

Container/Presentational - podział odpowiedzialności

Jednym z klasycznych wzorców kompozycji jest rozdzielenie komponentów na dwa rodzaje:

  1. Container (kontener) - odpowiada za logikę: pobieranie danych, zarządzanie stanem, obsługę zdarzeń
  2. Presentational (prezentacyjny) - odpowiada wyłącznie za wygląd: renderuje otrzymane dane, nie zarządza stanem

Wyobraź sobie to jak podział pracy na stacji kosmicznej: komputer pokładowy (kontener) przetwarza dane i podejmuje decyzje, a panele wyświetlaczy (prezentacyjne) pokazują te informacje załodze.

1// Komponent prezentacyjny - tylko wyświetla dane
2function CrewList({ members, onSelectMember }) {
3  return (
4    <ul className="crew-list">
5      {members.map(member => (
6        <li key={member.id} onClick={() => onSelectMember(member.id)}>
7          <span>{member.name}</span>
8          <span className="role">{member.role}</span>
9        </li>
10      ))}
11    </ul>
12  );
13}
14
15// Komponent kontenerowy - zarządza danymi i logiką
16function CrewListContainer() {
17  const [members, setMembers] = useState([]);
18  const [selectedId, setSelectedId] = useState(null);
19
20  useEffect(() => {
21    // Pobieranie danych załogi z API
22    fetchCrewData().then(data => setMembers(data));
23  }, []);
24
25  const handleSelectMember = (id) => {
26    setSelectedId(id);
27    logActivity(`Wybrano członka załogi: ${id}`);
28  };
29
30  return (
31    <CrewList
32      members={members}
33      onSelectMember={handleSelectMember}
34    />
35  );
36}

Dzięki temu podziałowi komponent CrewList jest łatwy do przetestowania i wielokrotnego użycia - wystarczy przekazać mu inne dane.

Higher-Order Components (HOC)

Higher-Order Component to funkcja, która przyjmuje komponent i zwraca nowy, rozszerzony komponent. Nazwa pochodzi od "higher-order functions" w programowaniu funkcyjnym. HOC to jak moduł ulepszający na stacji kosmicznej - dokładasz go do istniejącego modułu i ten zyskuje nowe możliwości, bez zmiany swojego wnętrza.

1// HOC dodający funkcję ładowania danych
2function withMissionData(WrappedComponent) {
3  return function EnhancedComponent(props) {
4    const [data, setData] = useState(null);
5    const [loading, setLoading] = useState(true);
6
7    useEffect(() => {
8      fetchMissionData(props.missionId)
9        .then(result => {
10          setData(result);
11          setLoading(false);
12        });
13    }, [props.missionId]);
14
15    if (loading) {
16      return <div className="loading">Ładowanie danych misji...</div>;
17    }
18
19    return <WrappedComponent {...props} missionData={data} />;
20  };
21}
22
23// Prosty komponent wyświetlający dane
24function MissionDetails({ missionData }) {
25  return (
26    <div>
27      <h2>{missionData.name}</h2>
28      <p>Cel: {missionData.destination}</p>
29    </div>
30  );
31}
32
33// Rozszerzony komponent z automatycznym ładowaniem danych
34const MissionDetailsWithData = withMissionData(MissionDetails);
35
36// Użycie - dane zostaną automatycznie pobrane
37<MissionDetailsWithData missionId="apollo-11" />

Konwencja nazewnicza HOC to prefiks with - withMissionData, withAuth, withLoading. Dzięki temu łatwo rozpoznać, że mamy do czynienia z komponentem rozszerzonym.

Render Props - przekazywanie funkcji renderującej

Wzorzec render props polega na przekazaniu komponentowi funkcji, która mówi mu, co ma renderować. To daje ogromną elastyczność - komponent zarządza logiką, a konsument decyduje o wyglądzie:

1function OrbitTracker({ satelliteId, render }) {
2  const [position, setPosition] = useState({ x: 0, y: 0, altitude: 400 });
3
4  useEffect(() => {
5    const interval = setInterval(() => {
6      // Symulacja zmiany pozycji na orbicie
7      setPosition(prev => ({
8        x: prev.x + Math.random() * 2 - 1,
9        y: prev.y + Math.random() * 2 - 1,
10        altitude: 400 + Math.sin(Date.now() / 1000) * 10
11      }));
12    }, 1000);
13    return () => clearInterval(interval);
14  }, [satelliteId]);
15
16  // Wywołuje funkcję renderującą z danymi
17  return render(position);
18}
19
20// Użycie - decydujemy jak wyświetlić dane pozycji
21function SatelliteMonitor() {
22  return (
23    <OrbitTracker
24      satelliteId="iss"
25      render={(position) => (
26        <div className="monitor">
27          <p>Pozycja X: {position.x.toFixed(2)}</p>
28          <p>Pozycja Y: {position.y.toFixed(2)}</p>
29          <p>Wysokość: {position.altitude.toFixed(1)} km</p>
30        </div>
31      )}
32    />
33  );
34}

Od atomów do stron - kompozycja w większej skali

Tę samą zasadę opisuje metoda Atomic Design: interfejs składa się z coraz większych części. Najpierw atomy (Button, Input, Label), z nich molekuły (SearchBar = Input + Button), z molekuł organizmy (Header = Logo + SearchBar + Nav), potem szablony (układ strony bez treści) i na końcu strony (szablon wypełniony prawdziwymi danymi).

Kiedy dzielić komponent na mniejsze?

Najważniejszy sygnał: komponent odpowiada za kilka niezwiązanych ze sobą rzeczy naraz, na przykład nawigację, formularz i listę. Zasada pojedynczej odpowiedzialności (Single Responsibility Principle) mówi, że każdy komponent powinien odpowiadać za jeden fragment interfejsu - jak moduł stacji, który ma jedno zadanie. Sama liczba linii kodu czy props nie przesądza o podziale.

Duży komponent refaktoryzujesz w czterech krokach:

  1. Znajdź w JSX niezależne sekcje (np. nagłówek, lista załogi, panel systemów).
  2. Ustal, jakich danych (props) potrzebuje każda sekcja.
  3. Wydziel każdą sekcję do osobnego komponentu.
  4. Połącz nowe komponenty w komponencie nadrzędnym.

Małe komponenty łatwiej też testować. Test jednostkowy sprawdza jeden komponent w izolacji (np. czy CrewList wyświetla wszystkich członków załogi), a test integracyjny sprawdza, czy kilka komponentów działa razem (np. czy SpaceStation przekazuje dane do CrewList).

Kiedy stosować poszczególne wzorce?

WzorzecZastosowanie
childrenProste kontenery, layouty, karty, panele
Sloty (props)Złożone layouty z wieloma obszarami treści
Container/PresentationalOddzielenie logiki od prezentacji
HOCWspółdzielenie logiki między wieloma komponentami
Render PropsElastyczne renderowanie z dostępem do stanu

Współcześnie wiele przypadków użycia HOC i Render Props zostało zastąpionych przez custom hooks, które poznamy w kolejnych modułach. Jednak znajomość tych wzorców jest ważna, ponieważ spotkasz je w istniejących kodach i bibliotekach.

Kod do tej lekcji: App.jsx
1import React from 'react';
2
3// =============================================
4// KOMPOZYCJA KOMPONENTÓW
5// Wzorce: children, wrapper components, HOC
6// =============================================
7
8// --- WZORZEC 1: Wrapper / Container z children ---
9// Card to komponent-kontener, który opakowuje dowolną zawartość
10function Card({ title, children, color = '#667eea' }) {
11  return (
12    <div style={{
13      border: `2px solid ${color}`,
14      borderRadius: '12px',
15      overflow: 'hidden',
16      marginBottom: '20px',
17      boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
18    }}>
19      {title && (
20        <div style={{
21          padding: '12px 20px',
22          backgroundColor: color,
23          color: 'white',
24          fontWeight: 'bold',
25          fontSize: '16px'
26        }}>
27          {title}
28        </div>
29      )}
30      <div style={{ padding: '20px' }}>
31        {children}
32      </div>
33    </div>
34  );
35}
36
37// Layout opakowuje całą stronę i zapewnia spójny wygląd
38function Layout({ header, children, footer }) {
39  return (
40    <div style={{
41      maxWidth: '800px',
42      margin: '0 auto',
43      minHeight: '100vh',
44      display: 'flex',
45      flexDirection: 'column',
46      fontFamily: 'Arial, sans-serif'
47    }}>
48      <header style={{
49        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
50        padding: '24px',
51        color: 'white',
52        textAlign: 'center'
53      }}>
54        {header}
55      </header>
56      <main style={{ flex: 1, padding: '24px', backgroundColor: '#f8f9fa' }}>
57        {children}
58      </main>
59      {footer && (
60        <footer style={{
61          padding: '16px',
62          backgroundColor: '#1a1a2e',
63          color: '#888',
64          textAlign: 'center',
65          fontSize: '14px'
66        }}>
67          {footer}
68        </footer>
69      )}
70    </div>
71  );
72}
73
74// --- WZORZEC 2: Higher-Order Component (HOC) ---
75// HOC to funkcja, która przyjmuje komponent i zwraca nowy,
76// wzbogacony komponent z dodatkową funkcjonalnością.
77
78function withLogger(WrappedComponent, componentLabel) {
79  return function LoggedComponent(props) {
80    React.useEffect(() => {
81      console.log(
82        `[LOG] <${componentLabel}> zamontowany z props:`,
83        props
84      );
85      return () => {
86        console.log(`[LOG] <${componentLabel}> odmontowany`);
87      };
88    }, []);
89
90    // Przekazujemy wszystkie oryginalne props + dodatkowe info
91    return <WrappedComponent {...props} isLogged={true} />;
92  };
93}
94
95// Zwykły komponent wyświetlający misję
96function MissionBadge({ name, status, priority, isLogged }) {
97  const statusColors = {
98    active: '#4caf50',
99    pending: '#ff9800',
100    completed: '#2196f3'
101  };
102
103  return (
104    <div style={{
105      display: 'flex',
106      alignItems: 'center',
107      justifyContent: 'space-between',
108      padding: '14px 18px',
109      backgroundColor: 'white',
110      borderRadius: '8px',
111      marginBottom: '10px',
112      boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
113    }}>
114      <div>
115        <strong style={{ fontSize: '16px' }}>{name}</strong>
116        <div style={{ color: '#888', fontSize: '13px', marginTop: '4px' }}>
117          Priorytet: {priority}
118        </div>
119      </div>
120      <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
121        {isLogged && (
122          <span style={{
123            fontSize: '11px',
124            color: '#888',
125            padding: '2px 6px',
126            border: '1px solid #ddd',
127            borderRadius: '4px'
128          }}>
129            LOGGED
130          </span>
131        )}
132        <span style={{
133          padding: '4px 12px',
134          borderRadius: '12px',
135          color: 'white',
136          backgroundColor: statusColors[status] || '#999',
137          fontSize: '12px',
138          fontWeight: 'bold'
139        }}>
140          {status.toUpperCase()}
141        </span>
142      </div>
143    </div>
144  );
145}
146
147// Tworzymy ulepszoną wersję MissionBadge za pomocą HOC
148const LoggedMissionBadge = withLogger(MissionBadge, 'MissionBadge');
149
150// --- GŁÓWNA APLIKACJA ---
151function App() {
152  const missions = [
153    { id: 1, name: 'Rekonesans Krateru Tycho', status: 'active', priority: 'Wysoki' },
154    { id: 2, name: 'Naprawa paneli słonecznych', status: 'pending', priority: 'Średni' },
155    { id: 3, name: 'Analiza próbek regolitu', status: 'completed', priority: 'Niski' },
156  ];
157
158  return (
159    <Layout
160      header={<h1 style={{ margin: 0 }}>Kompozycja Komponentów</h1>}
161      footer="Panel misji kosmicznych - React Composition Patterns"
162    >
163      {/* Card opakowuje listę misji */}
164      <Card title="Aktywne misje (z HOC withLogger)" color="#667eea">
165        <p style={{ color: '#555', marginTop: 0 }}>
166          Otwórz konsolę przeglądarki (F12) - zobaczysz logi z HOC
167          przy montowaniu każdego komponentu.
168        </p>
169        {missions.map((m) => (
170          <LoggedMissionBadge
171            key={m.id}
172            name={m.name}
173            status={m.status}
174            priority={m.priority}
175          />
176        ))}
177      </Card>
178
179      {/* Zagnieżdżone karty - kompozycja w kompozycji */}
180      <Card title="Zagnieżdżona kompozycja" color="#764ba2">
181        <p style={{ marginTop: 0 }}>
182          Card może zawierać inne Card - to siła wzorca children:
183        </p>
184        <Card title="Wewnętrzna karta A" color="#4caf50">
185          <p style={{ margin: 0 }}>Dowolna treść wewnątrz.</p>
186        </Card>
187        <Card title="Wewnętrzna karta B" color="#ff9800">
188          <p style={{ margin: 0 }}>Kolejna zagnieżdżona karta.</p>
189        </Card>
190      </Card>
191    </Layout>
192  );
193}
194
195export default App;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Kiedy warto użyć props.children w komponencie React?

  2. 2. Jak React przekazuje elementy zagnieżdżone wewnątrz komponentu do jego implementacji?

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż etapy kompozycji komponentów od najprostszego poziomu do najbardziej złożonego:

  • Edytor kodu

    Stwórz komponent CompleteMoonBase składający się z LunarModule, CrewQuarters i CommandCenter.

  • Układanie w pionie

    Ułóż hierarchię komponentów od zewnętrznego do wewnętrznego:

  • Układanie w pionie

    Ułóż wzorzec kompozycji komponentów (Atomic Design):

  • Klikanie w kolejności

    Zbuduj poprawną składnię eksportu komponentu React:

Przydatne artykuły