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.
In the browser DOM, events go through three phases:
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:
The event bubbles from child to parent - the exact opposite of what you might expect!
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.In addition to
stopPropagation() (stops bubbling), there is e.preventDefault() - which prevents the browser's default behavior. These two methods do completely different things:| Method | Action | |--------|--------| |
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 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}Event delegation is particularly efficient with large lists - instead of 100 separate listeners, you have one on the parent. React optimizes events internally anyway, but being aware of this pattern helps you write cleaner code.
A common pattern is passing additional data to an event handler. Instead of creating a handler directly in JSX, use an arrow function or create a dedicated function:
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: arrow function in JSX
12 // onClick={() => handleSelect(missionName)}
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
handleSelect(arg) => () => {...} pattern (currying) is a cleaner alternative to inline arrow functions. Avoid creating new functions in render when possible - it helps maintain code readability.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.Event propagation and advanced event handling patterns are key skills for a React ship pilot:
Mastering these techniques will allow you to build precise interaction systems where every signal on the ship goes exactly where it should!