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.
React wykorzystuje koncepcję jednokierunkowego (lub jednokierunkowego przepływu w dół) przepływu danych. 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:
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:
MissionControl jest komponentem rodzica, który zawiera dane misjiMissionInfo i CrewDisplay poprzez propsSkoro 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ę updateStatusonStatusChange do komponentu StatusControls (dziecko)StatusControls wywołuje tę funkcję z nową wartością, gdy użytkownik klika przyciskTen 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" 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:
SpaceshipDashboardShieldSystem i WeaponSystem otrzymują tylko dane, które potrzebują oraz funkcje do aktualizacji tych danychTa technika jest szczególnie przydatna, gdy:
W React, szczególnie w kontekście formularzy, istnieją dwa podejścia do zarządzania danymi wejściowymi: komponenty kontrolowane i niekontrolowane.
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:
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:
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.
W miarę rozrastania się aplikacji, przepływ danych może stać się bardziej złożony. W takich przypadkach warto rozważyć:
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}Dla jeszcze bardziej złożonych aplikacji, można rozważyć biblioteki zarządzania stanem takie jak Redux, MobX czy Recoil. Oferują one bardziej zaawansowane mechanizmy do zarządzania stanem globalnym.
Zrozumienie przepływu danych w React jest fundamentem tworzenia przewidywalnych, łatwych w utrzymaniu aplikacji:
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.