Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Kompozycja komponentów

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}

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.

Przejdź do CodeWorlds