Kurs JavaScript i React · Moduł 3: Stan i zdarzenia
Propagacja zdarzeń i zaawansowane wzorce obsługi zdarzeń
W tej lekcji7
Na pokładzie statku kosmicznego React, zdarzenia nie poruszają się w próżni - podróżują przez hierarchię komponentów, jak sygnał alarmowy, który rozchodzi się od czujnika w maszynowni aż do głównego centrum dowodzenia. Zrozumienie propagacji zdarzeń (event propagation) pozwala precyzyjnie kontrolować, który komponent reaguje na sygnał i kiedy.
Jak zdarzenia propagują się w React
W przeglądarkowym DOMie zdarzenia przechodzą przez trzy fazy:
- Capture (przechwytywanie) - zdarzenie schodzi od korzenia dokumentu w dół do elementu docelowego
- Target - zdarzenie dociera do elementu, na którym nastąpiło kliknięcie
- Bubbling (bąbelkowanie) - zdarzenie wraca w górę, od elementu docelowego do korzenia
React domyślnie obsługuje zdarzenia w fazie bubbling - czyli zdarzenie "bąbelkuje" od dziecka do rodzica. Wyobraź sobie to jak alarm na statku: czujnik dymu w kajucie (element docelowy) uruchamia się, a sygnał bąbelkuje w górę - najpierw słyszy go komendant pokładu, potem szef inżynierów, a na końcu kapitan statku.
1function SpaceStation() {
2 const handleStationClick = () => {
3 console.log('3. Stacja kosmiczna: sygnał dotarł do centrum dowodzenia');
4 };
5
6 return (
7 <div onClick={handleStationClick} style={{ padding: '40px', background: '#1a1a3e' }}>
8 <h2>Stacja Kosmiczna</h2>
9 <Deck />
10 </div>
11 );
12}
13
14function Deck() {
15 const handleDeckClick = () => {
16 console.log('2. Pokład: sygnał przechodzi przez pokład');
17 };
18
19 return (
20 <div onClick={handleDeckClick} style={{ padding: '30px', background: '#2a2a5e' }}>
21 <p>Pokład główny</p>
22 <Cabin />
23 </div>
24 );
25}
26
27function Cabin() {
28 const handleCabinClick = () => {
29 console.log('1. Kajuta: czujnik aktywowany!');
30 };
31
32 return (
33 <button onClick={handleCabinClick}>
34 Czujnik w kajucie
35 </button>
36 );
37}Po kliknięciu przycisku "Czujnik w kajucie" w konsoli pojawi się:
- Kajuta: czujnik aktywowany!
- Pokład: sygnał przechodzi przez pokład
- Stacja kosmiczna: sygnał dotarł do centrum dowodzenia
Zdarzenie bąbelkuje od dziecka do rodzica - dokładnie odwrotnie niż mogłoby się wydawać!
Zatrzymywanie propagacji - stopPropagation()
Czasem chcesz, żeby sygnał nie wychodził poza dany moduł statku. Metoda e.stopPropagation() zatrzymuje bąbelkowanie:
1function ControlPanel() {
2 const handlePanelClick = () => {
3 console.log('Panel: to się NIE wywoła');
4 };
5
6 return (
7 <div onClick={handlePanelClick} style={{ padding: '20px' }}>
8 <h3>Panel sterowania</h3>
9 <EmergencyButton />
10 </div>
11 );
12}
13
14function EmergencyButton() {
15 const handleEmergency = (e) => {
16 e.stopPropagation(); // Zatrzymaj propagację!
17 console.log('Przycisk awaryjny: sygnał przechwycony lokalnie');
18 };
19
20 return (
21 <button onClick={handleEmergency}>
22 Przycisk awaryjny
23 </button>
24 );
25}Po kliknięciu "Przycisk awaryjny" tylko handler handleEmergency zostanie wywołany - zdarzenie nie dotrze do rodzica ControlPanel.
Zapobieganie domyślnemu zachowaniu - preventDefault()
Oprócz stopPropagation() (zatrzymuje bąbelkowanie), istnieje e.preventDefault() - zapobiega domyślnemu zachowaniu przeglądarki. Te dwie metody robią zupełnie różne rzeczy:
| Metoda | Działanie |
|---|---|
e.stopPropagation() | Zatrzymuje bąbelkowanie do elementów nadrzędnych |
e.preventDefault() | Blokuje domyślne zachowanie przeglądarki (np. przesłanie formularza) |
1function MissionForm() {
2 const [missionName, setMissionName] = useState('');
3
4 const handleSubmit = (e) => {
5 e.preventDefault(); // Zapobiegaj przeładowaniu strony!
6 console.log('Misja zgłoszona:', missionName);
7 };
8
9 return (
10 <form onSubmit={handleSubmit}>
11 <input
12 value={missionName}
13 onChange={(e) => setMissionName(e.target.value)}
14 placeholder="Nazwa misji"
15 />
16 <button type="submit">Zgłoś misję</button>
17 </form>
18 );
19}Bez e.preventDefault() formularz spowodowałby przeładowanie strony - domyślne zachowanie przeglądarki dla elementu <form>.
Delegacja zdarzeń (Event Delegation)
Delegacja zdarzeń to wzorzec, w którym jeden handler na rodzicu obsługuje zdarzenia ze wszystkich dzieci. To jak jeden oficer na mostku obsługujący wszystkie sygnały z poszczególnych sektorów zamiast przydzielania osobnego oficera do każdego czujnika:
1function CrewList() {
2 const crew = [
3 { id: 1, name: 'Kapitan Nova', role: 'Dowodzenie' },
4 { id: 2, name: 'Inżynier Astro', role: 'Napęd warpowy' },
5 { id: 3, name: 'Nawigator Ra', role: 'Nawigacja' },
6 { id: 4, name: 'Medyk Luna', role: 'Oddział medyczny' },
7 ];
8
9 // Jeden handler na rodzicu zamiast osobnego na każdym elemencie
10 const handleCrewClick = (e) => {
11 const crewId = e.target.closest('[data-crew-id]')?.dataset.crewId;
12 if (crewId) {
13 const member = crew.find(c => c.id === Number(crewId));
14 console.log('Wybrany członek załogi:', member.name);
15 }
16 };
17
18 return (
19 <ul onClick={handleCrewClick}>
20 {crew.map(member => (
21 <li key={member.id} data-crew-id={member.id}>
22 {member.name} - {member.role}
23 </li>
24 ))}
25 </ul>
26 );
27}W czystym JavaScripcie delegacja oszczędza setki osobnych nasłuchiwaczy przy dużych listach. React robi to za ciebie: od wersji 17 podpina po jednym nasłuchiwaczu każdego typu zdarzenia do korzenia aplikacji, więc onClick na 100 elementach <li> nie tworzy 100 nasłuchiwaczy w DOM. W React delegacja przydaje się więc wtedy, gdy jeden handler ma rozpoznać kliknięty element (np. po atrybucie data-*), a nie dla wydajności.
Przekazywanie argumentów do handlerów
Częstym wzorcem jest przekazywanie dodatkowych danych do handlera zdarzenia. Możesz owinąć wywołanie funkcją strzałkową w JSX albo przygotować funkcję, która zwraca gotowy handler (currying):
1function MissionSelector() {
2 const missions = ['Apollo', 'Artemis', 'Voyager', 'Orion'];
3 const [selected, setSelected] = useState(null);
4
5 // Wzorzec: funkcja zwracająca handler
6 const handleSelect = (missionName) => () => {
7 setSelected(missionName);
8 console.log('Wybrano misję:', missionName);
9 };
10
11 // Alternatywnie bez curryingu: const selectMission = (missionName) => setSelected(missionName);
12 // i w JSX: onClick={() => selectMission(mission)}
13
14 return (
15 <div>
16 <h3>Wybierz misję:</h3>
17 {missions.map(mission => (
18 <button key={mission} onClick={handleSelect(mission)}>
19 {mission}
20 </button>
21 ))}
22 {selected && <p>Aktywna misja: {selected}</p>}
23 </div>
24 );
25}Wzorzec const handleSelect = (arg) => () => {...} (currying) to inny zapis tego samego: handleSelect(mission) też tworzy nową funkcję dla każdego przycisku przy każdym renderze, tak jak onClick={() => selectMission(mission)}. Wybierz zapis, który jest dla ciebie czytelniejszy - tworzenie funkcji w renderze jest w React normalne i nie trzeba go unikać.
Obsługa wielu zdarzeń jednym handlerem
Gdy komponent obsługuje wiele powiązanych zdarzeń, możesz użyć jednego handlera z rozróżnieniem po event.target.name lub event.target.dataset:
1function ShipControls() {
2 const [systems, setSystems] = useState({
3 engines: false,
4 shields: false,
5 weapons: false,
6 });
7
8 const handleToggle = (e) => {
9 const systemName = e.target.name;
10 setSystems(prev => ({
11 ...prev,
12 [systemName]: !prev[systemName]
13 }));
14 };
15
16 return (
17 <div>
18 <h3>Systemy statku:</h3>
19 {Object.entries(systems).map(([name, isActive]) => (
20 <label key={name} style={{ display: 'block', margin: '8px 0' }}>
21 <input
22 type="checkbox"
23 name={name}
24 checked={isActive}
25 onChange={handleToggle}
26 />
27 {name}: {isActive ? 'AKTYWNY' : 'WYŁĄCZONY'}
28 </label>
29 ))}
30 </div>
31 );
32}Dzięki [systemName]: !prev[systemName] (computed property name) jeden handler obsługuje wszystkie checkboxy. Atrybut name na inpucie identyfikuje, który system ma zostać przełączony.
Podsumowanie
Propagacja zdarzeń i zaawansowane wzorce obsługi zdarzeń to kluczowe umiejętności pilota statku React:
- Bąbelkowanie - zdarzenia propagują od dziecka do rodzica (domyślnie w React)
- stopPropagation() - zatrzymuje propagację do elementów nadrzędnych
- preventDefault() - blokuje domyślne zachowanie przeglądarki (np. submit formularza)
- Delegacja zdarzeń - jeden handler na rodzicu zamiast wielu na dzieciach
- Przekazywanie argumentów do handlerów - funkcja strzałkowa w JSX albo currying
- Computed property names - jeden handler dla wielu powiązanych elementów
Opanowanie tych technik pozwoli ci budować precyzyjne systemy interakcji, gdzie każdy sygnał na statku trafia dokładnie tam, gdzie powinien!
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2
3function EventLog({ logs }) {
4 return (
5 <div style={{
6 background: '#0d1117', borderRadius: '8px', padding: '12px',
7 maxHeight: '120px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '12px'
8 }}>
9 <div style={{ color: '#8b949e', marginBottom: '4px' }}>Konsola zdarzeń:</div>
10 {logs.length === 0 && <div style={{ color: '#484f58' }}>Kliknij elementy, aby zobaczyć propagację...</div>}
11 {logs.map((log, i) => (
12 <div key={i} style={{ color: log.color, padding: '2px 0' }}>
13 {log.text}
14 </div>
15 ))}
16 </div>
17 );
18}
19
20function SpaceStation() {
21 const [logs, setLogs] = useState([]);
22 const [stopPropagation, setStopPropagation] = useState(false);
23
24 const addLog = (text, color) => {
25 setLogs(prev => [...prev, { text, color }]);
26 };
27
28 const handleStationClick = () => {
29 addLog('3. Stacja kosmiczna: sygnał dotarł do centrum dowodzenia', '#f39c12');
30 };
31
32 const handleDeckClick = (e) => {
33 addLog('2. Pokład główny: sygnał przechodzi przez pokład', '#3498db');
34 };
35
36 const handleCabinClick = (e) => {
37 if (stopPropagation) {
38 e.stopPropagation();
39 addLog('1. Kajuta: sygnał ZATRZYMANY (stopPropagation)', '#e74c3c');
40 } else {
41 addLog('1. Kajuta: czujnik aktywowany!', '#2ecc71');
42 }
43 };
44
45 const handleClear = (e) => {
46 e.stopPropagation();
47 setLogs([]);
48 };
49
50 const styles = {
51 container: { background: '#0a0e1a', color: '#e0e8ff', fontFamily: 'sans-serif', padding: '20px', minHeight: '100vh' },
52 title: { color: '#4fc3f7', fontSize: '18px', marginBottom: '16px', textAlign: 'center' },
53 station: { background: '#1a1a3e', borderRadius: '12px', padding: '20px', border: '2px solid #4fc3f7', cursor: 'pointer' },
54 deck: { background: '#2a2a5e', borderRadius: '8px', padding: '16px', marginTop: '12px', border: '1px solid #6c6cae', cursor: 'pointer' },
55 cabin: {
56 display: 'block', width: '100%', marginTop: '12px', padding: '12px',
57 background: '#27ae60', color: 'white', border: 'none', borderRadius: '6px',
58 fontSize: '14px', cursor: 'pointer', fontWeight: 'bold'
59 },
60 toggle: {
61 display: 'flex', alignItems: 'center', gap: '8px',
62 margin: '16px 0', padding: '10px', background: '#1a2035', borderRadius: '6px'
63 },
64 clearBtn: {
65 background: '#333', color: '#aaa', border: '1px solid #555', borderRadius: '4px',
66 padding: '4px 12px', cursor: 'pointer', fontSize: '12px', marginTop: '8px'
67 }
68 };
69
70 return (
71 <div style={styles.container}>
72 <h2 style={styles.title}>Propagacja zdarzeń w React</h2>
73
74 <label style={styles.toggle}>
75 <input type="checkbox" checked={stopPropagation} onChange={() => setStopPropagation(prev => !prev)} />
76 <span style={{ color: stopPropagation ? '#e74c3c' : '#aaa' }}>
77 stopPropagation() {stopPropagation ? 'AKTYWNE' : 'wyłączone'}
78 </span>
79 </label>
80
81 <div style={styles.station} onClick={handleStationClick}>
82 <div style={{ color: '#f39c12', fontSize: '12px' }}>onClick: Stacja Kosmiczna</div>
83 <div style={styles.deck} onClick={handleDeckClick}>
84 <div style={{ color: '#3498db', fontSize: '12px' }}>onClick: Pokład główny</div>
85 <button style={styles.cabin} onClick={handleCabinClick}>
86 Kliknij: Czujnik w kajucie
87 </button>
88 </div>
89 </div>
90
91 <div style={{ marginTop: '16px' }}>
92 <EventLog logs={logs} />
93 <button style={styles.clearBtn} onClick={handleClear}>Wyczyść konsolę</button>
94 </div>
95 </div>
96 );
97}
98
99export default function App() {
100 return <SpaceStation />;
101}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. W jakiej kolejności wywołują się handlery onClick przy kliknięciu elementu zagnieżdżonego w React (domyślnie)?
2. Jaka jest różnica między e.stopPropagation() a e.preventDefault()?
Zadania praktyczne w grze
- Edytor kodu
Stwórz 3 stany logiczne: [lifeSupport, setLifeSupport] = useState(true), [powerGrid, setPowerGrid] = useState(true) i [communication, setCommunication] = useState(false). Dla każdego systemu dodaj przycisk, który przełącza jego stan (prev => !prev), i wyświetl status, np. Podtrzymywanie życia: {lifeSupport ? 'ON' : 'OFF'}. Kolor statusu zależy od stanu: zielony dla ON, czerwony dla OFF.
- Układanie w pionie
Uporządkuj fazy propagacji zdarzenia w przeglądarce od początku do końca:
- Układanie w poziomie
Ułóż składnię zatrzymania propagacji zdarzenia w React:
- Edytor kodu
Stwórz komponent MissionProtocols i wyrenderuj go w App. W stanie trzymaj tablicę protokołów { id, text, done }. Kontrolowane pole tekstowe i przycisk Dodaj dodają nowy protokół (nowa tablica przez spread). Każdy protokół ma checkbox, który przełącza done (map), i przycisk Usuń, który usuwa tylko ten protokół (filter). Wykonane protokoły wyróżnij stylem, np. textDecoration: 'line-through'.