Kurs JavaScript i React · Moduł 3: Stan i zdarzenia
Przepływ danych
W tej lekcji7
W kosmicznej podróży przez React, zrozumienie przepływu danych jest jak poznanie map gwiezdnych systemów - kluczowe dla bezpiecznej i efektywnej żeglugi. React opiera się na jednokierunkowym przepływie danych, który zapewnia przewidywalność i ułatwia debugowanie aplikacji.
Jednokierunkowy przepływ danych
React wykorzystuje jednokierunkowy przepływ danych (z góry na dół). Oznacza to, że dane przepływają tylko w jednym kierunku: od komponentów nadrzędnych (rodziców) do komponentów podrzędnych (dzieci).
Wyobraź sobie strukturę swojej aplikacji jak hierarchię dowodzenia w misji kosmicznej:
- Centrum dowodzenia (komponent najwyższego poziomu) wysyła instrukcje i dane do różnych modułów
- Każdy moduł może przekazać te dane dalej do swoich podmodułów
- Podmoduły nie mogą bezpośrednio modyfikować danych otrzymanych od modułów nadrzędnych
- Aby zmienić dane, podmoduły muszą "raportować" zmiany do modułów nadrzędnych, które mogą zaktualizować dane i przekazać je z powrotem
Zalety jednokierunkowego przepływu danych
- Przewidywalność - łatwiej zrozumieć skąd pochodzą dane i jak się zmieniają
- Łatwiejsze debugowanie - problemy z danymi można śledzić "pod prąd" do źródła
- Lepsza wydajność - React może zoptymalizować aktualizacje komponentów
- Mniejsza złożoność - każdy komponent ma jasno określoną rolę w przepływie danych
Props jako mechanizm przekazywania danych w dół
Głównym mechanizmem przekazywania danych od rodzica do dzieci są props (właściwości):
1// Komponent rodzica
2function MissionControl() {
3 const missionData = {
4 name: "Apollo 11",
5 destination: "Księżyc",
6 crew: ["Neil Armstrong", "Buzz Aldrin", "Michael Collins"],
7 status: "W trakcie"
8 };
9
10 return (
11 <div className="mission-control">
12 <h1>Centrum Kontroli Misji</h1>
13 <MissionInfo
14 name={missionData.name}
15 destination={missionData.destination}
16 status={missionData.status}
17 />
18 <CrewDisplay crew={missionData.crew} />
19 </div>
20 );
21}
22
23// Komponent dziecka
24function MissionInfo({ name, destination, status }) {
25 return (
26 <div className="mission-info">
27 <h2>{name}</h2>
28 <p>Cel: {destination}</p>
29 <p>Status: {status}</p>
30 </div>
31 );
32}
33
34// Inny komponent dziecka
35function CrewDisplay({ crew }) {
36 return (
37 <div className="crew-display">
38 <h2>Załoga</h2>
39 <ul>
40 {crew.map((member, index) => (
41 <li key={index}>{member}</li>
42 ))}
43 </ul>
44 </div>
45 );
46}W tym przykładzie:
MissionControljest komponentem rodzica, który zawiera dane misji- Przekazuje te dane w dół do komponentów
MissionInfoiCrewDisplaypoprzez props - Komponenty dzieci odbierają te dane i renderują je, ale nie mogą ich bezpośrednio modyfikować
Komunikacja w górę - jak dzieci komunikują się z rodzicami
Skoro dane płyną tylko w dół, jak komponenty niższego poziomu mogą komunikować się z komponentami wyższego poziomu? Rozwiązaniem jest technika polegająca na przekazywaniu funkcji w dół jako props, które komponenty dzieci mogą wywołać, aby "zgłosić" zmiany do rodzica.
1function MissionControl() {
2 const [missionStatus, setMissionStatus] = useState("Przygotowanie");
3
4 // Ta funkcja będzie przekazana dziecku
5 const updateStatus = (newStatus) => {
6 setMissionStatus(newStatus);
7 console.log(`Status misji zaktualizowany na: ${newStatus}`);
8 };
9
10 return (
11 <div className="mission-control">
12 <h1>Centrum Kontroli Misji</h1>
13 <p>Aktualny status: {missionStatus}</p>
14
15 {/* Przekazujemy funkcję updateStatus jako prop do StatusControls */}
16 <StatusControls
17 currentStatus={missionStatus}
18 onStatusChange={updateStatus}
19 />
20 </div>
21 );
22}
23
24function StatusControls({ currentStatus, onStatusChange }) {
25 return (
26 <div className="status-controls">
27 <h2>Kontrola statusu</h2>
28
29 <button
30 onClick={() => onStatusChange("Odliczanie")}
31 disabled={currentStatus === "Odliczanie"}
32 >
33 Rozpocznij odliczanie
34 </button>
35
36 <button
37 onClick={() => onStatusChange("Start")}
38 disabled={currentStatus === "Start" || currentStatus !== "Odliczanie"}
39 >
40 Inicjuj start
41 </button>
42
43 <button
44 onClick={() => onStatusChange("Orbita")}
45 disabled={currentStatus === "Orbita" || currentStatus !== "Start"}
46 >
47 Potwierdź osiągnięcie orbity
48 </button>
49
50 <button
51 onClick={() => onStatusChange("Przerwano")}
52 disabled={currentStatus === "Przerwano" || currentStatus === "Orbita"}
53 >
54 Przerwij misję
55 </button>
56 </div>
57 );
58}W tym przykładzie:
MissionControl(rodzic) definiuje funkcjęupdateStatus- Przekazuje tę funkcję jako prop
onStatusChangedo komponentuStatusControls(dziecko) StatusControlswywołuje tę funkcję z nową wartością, gdy użytkownik klika przycisk- Po wywołaniu, funkcja aktualizuje stan w komponencie rodzica, co prowadzi do ponownego renderowania
- Zaktualizowane dane (nowy status) przepływają z powrotem w dół do dzieci poprzez props
Ten wzorzec pozwala na zachowanie jednokierunkowego przepływu danych, mimo że komponenty niższego poziomu mogą wpływać na dane w komponentach wyższego poziomu.
Lifting state up (wynoszenie stanu w górę)
"Lifting state up" to ważna koncepcja w React, polegająca na przeniesieniu stanu z komponentów potomnych do najbliższego wspólnego przodka, który potrzebuje dostępu do tych danych. Ta technika jest kluczowa dla zachowania jednokierunkowego przepływu danych.
Rozważmy przykład z modułami statku kosmicznego, które muszą współdzielić dane:
1function SpaceshipDashboard() {
2 // Stan został "wyniesiony w górę" do wspólnego rodzica
3 const [power, setPower] = useState(100);
4 const [shields, setShields] = useState(75);
5 const [weapons, setWeapons] = useState(0);
6
7 // Funkcje do aktualizacji stanu
8 const adjustPower = (system, amount) => {
9 if (system === 'shields') {
10 setShields(prev => Math.min(100, Math.max(0, prev + amount)));
11 setPower(prev => Math.max(0, prev - amount));
12 } else if (system === 'weapons') {
13 setWeapons(prev => Math.min(100, Math.max(0, prev + amount)));
14 setPower(prev => Math.max(0, prev - amount));
15 }
16 };
17
18 return (
19 <div className="spaceship-dashboard">
20 <h1>Panel kontrolny statku kosmicznego</h1>
21
22 <PowerDisplay powerLevel={power} />
23
24 <div className="systems">
25 <ShieldSystem
26 shieldLevel={shields}
27 onAdjustPower={amount => adjustPower('shields', amount)}
28 />
29
30 <WeaponSystem
31 weaponCharge={weapons}
32 onAdjustPower={amount => adjustPower('weapons', amount)}
33 />
34 </div>
35 </div>
36 );
37}
38
39function PowerDisplay({ powerLevel }) {
40 return (
41 <div className="power-display">
42 <h2>Rezerwa mocy: {powerLevel}%</h2>
43 <div className="power-bar" style={{ width: `${powerLevel}%` }}></div>
44 </div>
45 );
46}
47
48function ShieldSystem({ shieldLevel, onAdjustPower }) {
49 return (
50 <div className="system-panel shields">
51 <h2>Tarcze: {shieldLevel}%</h2>
52 <div className="shield-bar" style={{ height: `${shieldLevel}%` }}></div>
53
54 <div className="controls">
55 <button onClick={() => onAdjustPower(10)} disabled={shieldLevel >= 100}>
56 + Zwiększ moc tarcz
57 </button>
58 <button onClick={() => onAdjustPower(-10)} disabled={shieldLevel <= 0}>
59 - Zmniejsz moc tarcz
60 </button>
61 </div>
62 </div>
63 );
64}
65
66function WeaponSystem({ weaponCharge, onAdjustPower }) {
67 return (
68 <div className="system-panel weapons">
69 <h2>Ładowanie broni: {weaponCharge}%</h2>
70 <div className="weapon-bar" style={{ height: `${weaponCharge}%` }}></div>
71
72 <div className="controls">
73 <button onClick={() => onAdjustPower(25)} disabled={weaponCharge >= 100}>
74 Ładuj broń
75 </button>
76 <button onClick={() => onAdjustPower(-25)} disabled={weaponCharge <= 0}>
77 Rozładuj broń
78 </button>
79 </div>
80 </div>
81 );
82}W tym przykładzie:
- Stan dotyczący mocy, tarcz i broni jest przechowywany w komponencie nadrzędnym
SpaceshipDashboard - Komponenty
ShieldSystemiWeaponSystemotrzymują tylko dane, które potrzebują oraz funkcje do aktualizacji tych danych - Gdy jeden system wykorzystuje moc, wpływa to na dostępną moc dla innych systemów
- Logika współdzielenia zasobów jest zarządzana centralnie przez komponent nadrzędny
Ta technika jest szczególnie przydatna, gdy:
- Wiele komponentów potrzebuje tych samych danych
- Dane w jednym komponencie wpływają na dane w innych komponentach
- Komponenty muszą się synchronizować
Komponenty kontrolowane i niekontrolowane
W React, szczególnie w kontekście formularzy, istnieją dwa podejścia do zarządzania danymi wejściowymi: komponenty kontrolowane i niekontrolowane.
Komponenty kontrolowane
W komponentach kontrolowanych, dane formularza są zarządzane przez stan React. Każda zmiana wprowadzona przez użytkownika przechodzi przez React:
1function ControlledMissionForm() {
2 const [formData, setFormData] = useState({
3 missionName: '',
4 duration: '',
5 crew: ''
6 });
7
8 const handleChange = (e) => {
9 setFormData({
10 ...formData,
11 [e.target.name]: e.target.value
12 });
13 };
14
15 const handleSubmit = (e) => {
16 e.preventDefault();
17 console.log('Dane misji:', formData);
18 // Logika wysyłania danych...
19 };
20
21 return (
22 <form onSubmit={handleSubmit}>
23 <div>
24 <label>
25 Nazwa misji:
26 <input
27 type="text"
28 name="missionName"
29 value={formData.missionName}
30 onChange={handleChange}
31 />
32 </label>
33 </div>
34
35 <div>
36 <label>
37 Czas trwania (dni):
38 <input
39 type="number"
40 name="duration"
41 value={formData.duration}
42 onChange={handleChange}
43 />
44 </label>
45 </div>
46
47 <div>
48 <label>
49 Liczba załogantów:
50 <input
51 type="number"
52 name="crew"
53 value={formData.crew}
54 onChange={handleChange}
55 />
56 </label>
57 </div>
58
59 <button type="submit">Zaplanuj misję</button>
60 </form>
61 );
62}Zalety komponentów kontrolowanych:
- Pełna kontrola nad stanem i walidacją w czasie rzeczywistym
- Możliwość natychmiastowej reakcji na zmiany (np. dynamiczna walidacja)
- Łatwość w manipulowaniu i transformowaniu danych wejściowych
Komponenty niekontrolowane
Komponenty niekontrolowane pozwalają samemu DOM zarządzać stanem formularza:
1function UncontrolledMissionForm() {
2 // Referencje do elementów DOM
3 const missionNameRef = useRef();
4 const durationRef = useRef();
5 const crewRef = useRef();
6
7 const handleSubmit = (e) => {
8 e.preventDefault();
9
10 const formData = {
11 missionName: missionNameRef.current.value,
12 duration: durationRef.current.value,
13 crew: crewRef.current.value
14 };
15
16 console.log('Dane misji:', formData);
17 // Logika wysyłania danych...
18 };
19
20 return (
21 <form onSubmit={handleSubmit}>
22 <div>
23 <label>
24 Nazwa misji:
25 <input
26 type="text"
27 name="missionName"
28 ref={missionNameRef}
29 defaultValue="Misja Mars"
30 />
31 </label>
32 </div>
33
34 <div>
35 <label>
36 Czas trwania (dni):
37 <input
38 type="number"
39 name="duration"
40 ref={durationRef}
41 defaultValue="90"
42 />
43 </label>
44 </div>
45
46 <div>
47 <label>
48 Liczba załogantów:
49 <input
50 type="number"
51 name="crew"
52 ref={crewRef}
53 defaultValue="4"
54 />
55 </label>
56 </div>
57
58 <button type="submit">Zaplanuj misję</button>
59 </form>
60 );
61}Zalety komponentów niekontrolowanych:
- Mniejsza ilość kodu dla prostych formularzy
- Mniej renderowań (lepsza wydajność w niektórych przypadkach)
- Łatwiejsza integracja z bibliotekami zewnętrznymi
W większości przypadków, zaleca się używanie komponentów kontrolowanych, ponieważ zapewniają one większą kontrolę i są bardziej zgodne z filozofią Reacta. Jednak w niektórych scenariuszach (np. przesyłanie plików, integracja z bibliotekami zewnętrznymi) komponenty niekontrolowane mogą być bardziej praktyczne.
Przepływ danych w większych aplikacjach
W miarę rozrastania się aplikacji, przepływ danych może stać się bardziej złożony. W takich przypadkach warto rozważyć:
1. Kontekst (Context API)
Dla danych, które są potrzebne w wielu miejscach w aplikacji, można użyć Context API, aby uniknąć tzw. "props drilling" (przekazywania props przez wiele poziomów komponentów):
1// Tworzenie kontekstu
2const MissionContext = React.createContext();
3
4// Komponent Provider na wysokim poziomie
5function MissionProvider({ children }) {
6 const [missionData, setMissionData] = useState({
7 name: "Artemis",
8 phase: "Przygotowanie",
9 crew: ["Anna", "Jan", "Maria", "Piotr"],
10 launchDate: "2025-07-20"
11 });
12
13 const updateMission = (updates) => {
14 setMissionData(prev => ({
15 ...prev,
16 ...updates
17 }));
18 };
19
20 return (
21 <MissionContext.Provider value={{ missionData, updateMission }}>
22 {children}
23 </MissionContext.Provider>
24 );
25}
26
27// Użycie w głęboko zagnieżdżonym komponencie
28function DeepNestedComponent() {
29 const { missionData, updateMission } = useContext(MissionContext);
30
31 return (
32 <div>
33 <h3>Faza misji: {missionData.phase}</h3>
34 <button onClick={() => updateMission({ phase: "Odliczanie" })}>
35 Rozpocznij odliczanie
36 </button>
37 </div>
38 );
39}
40
41// Struktura aplikacji
42function App() {
43 return (
44 <MissionProvider>
45 <div className="app">
46 <Header />
47 <MainDashboard />
48 <Footer />
49 </div>
50 </MissionProvider>
51 );
52}2. Zarządzanie stanem globalnym
Dla jeszcze bardziej złożonych aplikacji, można rozważyć biblioteki zarządzania stanem takie jak Redux Toolkit, Zustand czy MobX. Oferują one bardziej zaawansowane mechanizmy do zarządzania stanem globalnym.
Podsumowanie
Zrozumienie przepływu danych w React jest fundamentem tworzenia przewidywalnych, łatwych w utrzymaniu aplikacji:
- Jednokierunkowy przepływ danych - dane płyną z góry na dół, od rodziców do dzieci
- Props - podstawowy mechanizm przekazywania danych w dół
- Funkcje jako props - mechanizm komunikacji w górę
- Lifting state up - technika wynoszenia współdzielonego stanu do wspólnego przodka
- Komponenty kontrolowane vs niekontrolowane - różne podejścia do zarządzania danymi formularzy
- Context API i biblioteki zarządzania stanem - rozwiązania dla bardziej złożonych przepływów danych
Przestrzegając tych zasad, tworzysz aplikacje, które są łatwiejsze w zrozumieniu, debugowaniu i rozwijaniu - podobnie jak dobrze zaplanowana misja kosmiczna ma jasno określone protokoły komunikacji i przepływu danych.
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2
3// Komponent dziecka - otrzymuje dane przez props
4function MissionInfo({ mission, onStatusChange }) {
5 return (
6 <div className="mission-info">
7 <h3>{mission.name}</h3>
8 <p>Status: <span className={`status ${mission.status}`}>{mission.status}</span></p>
9 <p>Cel: {mission.destination}</p>
10 <p>Załoga: {mission.crew.join(', ')}</p>
11
12 <div className="controls">
13 <button
14 onClick={() => onStatusChange(mission.id, 'w trakcie')}
15 disabled={mission.status === 'w trakcie'}
16 >
17 Rozpocznij misję
18 </button>
19 <button
20 onClick={() => onStatusChange(mission.id, 'zakończona')}
21 disabled={mission.status === 'zakończona'}
22 >
23 Zakończ misję
24 </button>
25 <button
26 onClick={() => onStatusChange(mission.id, 'anulowana')}
27 disabled={mission.status === 'anulowana'}
28 >
29 Anuluj misję
30 </button>
31 </div>
32 </div>
33 );
34}
35
36// Komponent rodzica - zarządza stanem i przekazuje dane w dół
37function MissionControl() {
38 const [missions, setMissions] = useState([
39 {
40 id: 1,
41 name: 'Apollo 21',
42 status: 'planowana',
43 destination: 'Księżyc',
44 crew: ['Anna Kowalska', 'Jan Nowak']
45 },
46 {
47 id: 2,
48 name: 'Mars Explorer',
49 status: 'planowana',
50 destination: 'Mars',
51 crew: ['Maria Wiśniewska', 'Piotr Zieliński', 'Katarzyna Dąbrowska']
52 },
53 {
54 id: 3,
55 name: 'Titan Survey',
56 status: 'w trakcie',
57 destination: 'Tytan (księżyc Saturna)',
58 crew: ['Tomasz Kowalczyk', 'Agnieszka Nowak']
59 }
60 ]);
61
62 // Funkcja przekazywana w dół - komunikacja w górę
63 const handleStatusChange = (missionId, newStatus) => {
64 setMissions(prevMissions =>
65 prevMissions.map(mission =>
66 mission.id === missionId
67 ? { ...mission, status: newStatus }
68 : mission
69 )
70 );
71 };
72
73 const getStatusCount = (status) => {
74 return missions.filter(mission => mission.status === status).length;
75 };
76
77 return (
78 <div className="mission-control">
79 <h1>Centrum Kontroli Misji</h1>
80
81 <div className="statistics">
82 <div className="stat">
83 <span className="count">{getStatusCount('planowana')}</span>
84 <span className="label">Planowane</span>
85 </div>
86 <div className="stat">
87 <span className="count">{getStatusCount('w trakcie')}</span>
88 <span className="label">W trakcie</span>
89 </div>
90 <div className="stat">
91 <span className="count">{getStatusCount('zakończona')}</span>
92 <span className="label">Zakończone</span>
93 </div>
94 <div className="stat">
95 <span className="count">{getStatusCount('anulowana')}</span>
96 <span className="label">Anulowane</span>
97 </div>
98 </div>
99
100 <div className="missions-grid">
101 {missions.map(mission => (
102 <MissionInfo
103 key={mission.id}
104 mission={mission}
105 onStatusChange={handleStatusChange}
106 />
107 ))}
108 </div>
109 </div>
110 );
111}
112
113function App() {
114 return (
115 <div className="app">
116 <MissionControl />
117 </div>
118 );
119}
120
121export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Stwórz formularz logowania ze stanami [username, setUsername] i [password, setPassword]. Oba pola mają być kontrolowane, np. <input value={username} onChange={e => setUsername(e.target.value)} />, a pole hasła ma type="password". Formularz ma onSubmit={handleLogin}, a handleLogin wywołuje e.preventDefault(). Po wysłaniu wyświetl powitanie Witaj {username} (zapamiętaj w stanie, że formularz został wysłany).