Kurs JavaScript i React · Moduł 2: Komponenty i props
Kompozycja komponentów
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:
- Container (kontener) - odpowiada za logikę: pobieranie danych, zarządzanie stanem, obsługę zdarzeń
- 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:
- Znajdź w JSX niezależne sekcje (np. nagłówek, lista załogi, panel systemów).
- Ustal, jakich danych (props) potrzebuje każda sekcja.
- Wydziel każdą sekcję do osobnego komponentu.
- 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?
| Wzorzec | Zastosowanie |
|---|---|
children | Proste kontenery, layouty, karty, panele |
| Sloty (props) | Złożone layouty z wieloma obszarami treści |
| Container/Presentational | Oddzielenie logiki od prezentacji |
| HOC | Współdzielenie logiki między wieloma komponentami |
| Render Props | Elastyczne 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. Kiedy warto użyć props.children w komponencie React?
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: