Kurs JavaScript i React · Moduł 3: Stan i zdarzenia

Propagacja zdarzeń i zaawansowane wzorce obsługi zdarzeń

6 min czytania
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:

  1. Capture (przechwytywanie) - zdarzenie schodzi od korzenia dokumentu w dół do elementu docelowego
  2. Target - zdarzenie dociera do elementu, na którym nastąpiło kliknięcie
  3. 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ę:

  1. Kajuta: czujnik aktywowany!
  2. Pokład: sygnał przechodzi przez pokład
  3. 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:

MetodaDział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:

  1. Bąbelkowanie - zdarzenia propagują od dziecka do rodzica (domyślnie w React)
  2. stopPropagation() - zatrzymuje propagację do elementów nadrzędnych
  3. preventDefault() - blokuje domyślne zachowanie przeglądarki (np. submit formularza)
  4. Delegacja zdarzeń - jeden handler na rodzicu zamiast wielu na dzieciach
  5. Przekazywanie argumentów do handlerów - funkcja strzałkowa w JSX albo currying
  6. 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. 1. W jakiej kolejności wywołują się handlery onClick przy kliknięciu elementu zagnieżdżonego w React (domyślnie)?

  2. 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'.

Przydatne artykuły