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.
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ł.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.Jednym z klasycznych wzorców kompozycji jest rozdzielenie komponentów na dwa rodzaje:
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 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.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}| 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.