JavaScript and React course Β· Module 3: State and Events

Event Propagation and Advanced Event Handling Patterns

6 min read
In this lesson7

On board the React spaceship, events do not move in a vacuum - they travel through the component hierarchy, like an alarm signal that spreads from a sensor in the engine room all the way to the main command center. Understanding event propagation allows you to precisely control which component responds to the signal and when.

How events propagate in React

In the browser DOM, events go through three phases:

  1. Capture - the event descends from the document root down to the target element
  2. Target - the event reaches the element that was clicked
  3. Bubbling - the event travels back up, from the target element to the root

React handles events in the bubbling phase by default - meaning the event "bubbles" from child to parent. Imagine it like an alarm on a ship: the smoke detector in a cabin (target element) triggers, and the signal bubbles up - first the deck commander hears it, then the chief engineer, and finally the ship's captain.

1function SpaceStation() {
2  const handleStationClick = () => {
3    console.log('3. Space Station: signal reached command center');
4  };
5
6  return (
7    <div onClick={handleStationClick} style={{ padding: '40px', background: '#1a1a3e' }}>
8      <h2>Space Station</h2>
9      <Deck />
10    </div>
11  );
12}
13
14function Deck() {
15  const handleDeckClick = () => {
16    console.log('2. Deck: signal passing through deck');
17  };
18
19  return (
20    <div onClick={handleDeckClick} style={{ padding: '30px', background: '#2a2a5e' }}>
21      <p>Main Deck</p>
22      <Cabin />
23    </div>
24  );
25}
26
27function Cabin() {
28  const handleCabinClick = () => {
29    console.log('1. Cabin: sensor activated!');
30  };
31
32  return (
33    <button onClick={handleCabinClick}>
34      Cabin Sensor
35    </button>
36  );
37}

After clicking the "Cabin Sensor" button, the console will show:

  1. Cabin: sensor activated!
  2. Deck: signal passing through deck
  3. Space Station: signal reached command center

The event bubbles from child to parent - the exact opposite of what you might expect!

Stopping propagation - stopPropagation()

Sometimes you want the signal to not leave a given ship module. The e.stopPropagation() method stops bubbling:

1function ControlPanel() {
2  const handlePanelClick = () => {
3    console.log('Panel: this will NOT be called');
4  };
5
6  return (
7    <div onClick={handlePanelClick} style={{ padding: '20px' }}>
8      <h3>Control Panel</h3>
9      <EmergencyButton />
10    </div>
11  );
12}
13
14function EmergencyButton() {
15  const handleEmergency = (e) => {
16    e.stopPropagation(); // Stop propagation!
17    console.log('Emergency button: signal captured locally');
18  };
19
20  return (
21    <button onClick={handleEmergency}>
22      Emergency Button
23    </button>
24  );
25}

After clicking "Emergency Button", only the handleEmergency handler will be called - the event will not reach the parent ControlPanel.

Preventing default behavior - preventDefault()

In addition to stopPropagation() (stops bubbling), there is e.preventDefault() - which prevents the browser's default behavior. These two methods do completely different things:

MethodAction
e.stopPropagation()Stops bubbling to parent elements
e.preventDefault()Blocks the browser's default behavior (e.g., form submission)
1function MissionForm() {
2  const [missionName, setMissionName] = useState('');
3
4  const handleSubmit = (e) => {
5    e.preventDefault(); // Prevent page reload!
6    console.log('Mission submitted:', missionName);
7  };
8
9  return (
10    <form onSubmit={handleSubmit}>
11      <input
12        value={missionName}
13        onChange={(e) => setMissionName(e.target.value)}
14        placeholder="Mission name"
15      />
16      <button type="submit">Submit Mission</button>
17    </form>
18  );
19}

Without e.preventDefault(), the form would cause a page reload - the default browser behavior for the <form> element.

Event Delegation

Event delegation is a pattern where a single handler on the parent handles events from all children. It is like one officer on the bridge handling all signals from individual sectors instead of assigning a separate officer to each sensor:

1function CrewList() {
2  const crew = [
3    { id: 1, name: 'Captain Nova', role: 'Command' },
4    { id: 2, name: 'Engineer Astro', role: 'Warp Drive' },
5    { id: 3, name: 'Navigator Ra', role: 'Navigation' },
6    { id: 4, name: 'Medic Luna', role: 'Medical Bay' },
7  ];
8
9  // One handler on the parent instead of one on each element
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('Selected crew member:', 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}

In plain JavaScript, delegation saves hundreds of separate listeners on large lists. React does this for you: since version 17 it attaches one listener per event type to the app's root, so onClick on 100 <li> elements does not create 100 DOM listeners. In React, delegation is therefore useful when one handler needs to recognise the clicked element (e.g. by a data-* attribute), not for performance.

Passing arguments to handlers

A common pattern is passing additional data to an event handler. You can wrap the call in an arrow function in JSX or prepare a function that returns a ready handler (currying):

1function MissionSelector() {
2  const missions = ['Apollo', 'Artemis', 'Voyager', 'Orion'];
3  const [selected, setSelected] = useState(null);
4
5  // Pattern: function returning a handler
6  const handleSelect = (missionName) => () => {
7    setSelected(missionName);
8    console.log('Mission selected:', missionName);
9  };
10
11  // Alternatively, without currying: const selectMission = (missionName) => setSelected(missionName);
12  // and in JSX: onClick={() => selectMission(mission)}
13
14  return (
15    <div>
16      <h3>Select a Mission:</h3>
17      {missions.map(mission => (
18        <button key={mission} onClick={handleSelect(mission)}>
19          {mission}
20        </button>
21      ))}
22      {selected && <p>Active mission: {selected}</p>}
23    </div>
24  );
25}

The const handleSelect = (arg) => () => {...} pattern (currying) is just another way to write the same thing: handleSelect(mission) also creates a new function for every button on every render, just like onClick={() => selectMission(mission)}. Pick whichever reads better to you - creating functions during render is normal in React and there is no need to avoid it.

Handling multiple events with a single handler

When a component handles multiple related events, you can use a single handler with differentiation by event.target.name or 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>Ship Systems:</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 ? 'ACTIVE' : 'OFFLINE'}
28        </label>
29      ))}
30    </div>
31  );
32}

Thanks to [systemName]: !prev[systemName] (computed property name), a single handler manages all checkboxes. The name attribute on the input identifies which system should be toggled.

Summary

Event propagation and advanced event handling patterns are key skills for a React ship pilot:

  1. Bubbling - events propagate from child to parent (default in React)
  2. stopPropagation() - stops propagation to parent elements
  3. preventDefault() - blocks the browser's default behavior (e.g., form submit)
  4. Event delegation - one handler on the parent instead of many on children
  5. Passing arguments to handlers - an arrow function in JSX or currying
  6. Computed property names - one handler for multiple related elements

Mastering these techniques will allow you to build precise interaction systems where every signal on the ship goes exactly where it should!

Code for this lesson: 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' }}>Event console:</div>
10      {logs.length === 0 && <div style={{ color: '#484f58' }}>Click elements to see propagation...</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. Space station: signal reached command center', '#f39c12');
30  };
31
32  const handleDeckClick = (e) => {
33    addLog('2. Main deck: signal passes through the deck', '#3498db');
34  };
35
36  const handleCabinClick = (e) => {
37    if (stopPropagation) {
38      e.stopPropagation();
39      addLog('1. Cabin: signal STOPPED (stopPropagation)', '#e74c3c');
40    } else {
41      addLog('1. Cabin: sensor activated!', '#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}>Event Propagation in 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 ? 'ACTIVE' : 'disabled'}
78        </span>
79      </label>
80
81      <div style={styles.station} onClick={handleStationClick}>
82        <div style={{ color: '#f39c12', fontSize: '12px' }}>onClick: Space Station</div>
83        <div style={styles.deck} onClick={handleDeckClick}>
84          <div style={{ color: '#3498db', fontSize: '12px' }}>onClick: Main Deck</div>
85          <button style={styles.cabin} onClick={handleCabinClick}>
86            Click: Sensor in the cabin
87          </button>
88        </div>
89      </div>
90
91      <div style={{ marginTop: '16px' }}>
92        <EventLog logs={logs} />
93        <button style={styles.clearBtn} onClick={handleClear}>Clear console</button>
94      </div>
95    </div>
96  );
97}
98
99export default function App() {
100  return <SpaceStation />;
101}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. In what order are onClick handlers called when clicking a nested element in React (by default)?

  2. 2. What is the difference between e.stopPropagation() and e.preventDefault()?

Hands-on tasks in the game

  • Code editor

    Create 3 boolean states: [lifeSupport, setLifeSupport] = useState(true), [powerGrid, setPowerGrid] = useState(true) and [communication, setCommunication] = useState(false). For each system add a button that toggles its state (prev => !prev) and display the status, e.g. Life Support: {lifeSupport ? 'ON' : 'OFF'}. The status colour depends on the state: green for ON, red for OFF.

  • Vertical ordering

    Arrange the phases of event propagation in the browser from start to finish:

  • Horizontal ordering

    Arrange the syntax for stopping event propagation in React:

  • Code editor

    Create a MissionProtocols component and render it in App. Keep an array of protocols { id, text, done } in state. A controlled text field and an Add button add a new protocol (a new array with spread). Each protocol has a checkbox that toggles done (map) and a Delete button that removes only that protocol (filter). Highlight completed protocols with a style, e.g. textDecoration: 'line-through'.

Useful articles