JavaScript and React course Β· Module 3: State and Events
Event Handling
In this lesson8
On a cosmic journey through React, event handling is like a flight control system, allowing you to respond to various user actions. It is a key element of creating interactive interfaces that respond to user actions such as clicks, text input, or mouse movements.
Basics of event handling in React
In React, event handling works similarly to standard HTML, but with a few important differences:
Event names use camelCase convention instead of lowercase:
- HTML:
onclick,onmouseover - React:
onClick,onMouseOver
- HTML:
In JSX, we pass a function as the event handler, not a string:
- HTML:
<button onclick="handleClick()">Click</button> - React:
<button onClick={handleClick}>Click</button>
- HTML:
To prevent default behavior, we must explicitly call
preventDefault:- HTML:
<a href="#" onclick="return false">Click</a> - React:
<a href="#" onClick={(e) => e.preventDefault()}>Click</a>
- HTML:
Responding to user actions
Events are how React components respond to user actions. Let's look at the basic event types and how to handle them.
Handling clicks
The simplest and most commonly used event is onClick:
1function LaunchButton() {
2 const handleLaunch = () => {
3 console.log('Starting the countdown to launch!');
4 // Rocket launch logic...
5 };
6
7 return (
8 <button
9 className="launch-button"
10 onClick={handleLaunch}
11 >
12 Start Countdown
13 </button>
14 );
15}Arguments in event handlers
Every event handler receives an event object as its first argument. It contains information about the event and offers methods like preventDefault():
1function NavigationLink() {
2 const handleClick = (event) => {
3 event.preventDefault(); // Prevents the default action (e.g., navigating to another page)
4 console.log('Link clicked, but the page did not reload');
5
6 // Custom navigation or other logic...
7 };
8
9 return (
10 <a
11 href="/destination"
12 onClick={handleClick}
13 >
14 Go to Destination
15 </a>
16 );
17}The object React passes to the handler is a SyntheticEvent - a wrapper around the browser's native event. It has the same interface (target, preventDefault(), stopPropagation()) and works the same way in every browser; the original event is in event.nativeEvent. So the order is: the user clicks, the browser creates the event, React wraps it in a SyntheticEvent, and only then calls your handler.
Passing arguments to event handlers
Sometimes we need to pass additional arguments to an event handler. We can do this in two ways:
1. Using an arrow function
1function MissionControl({ missions }) {
2 const selectMission = (missionId, event) => {
3 console.log(`Selected mission with ID: ${missionId}`);
4 console.log('Event:', event);
5 // Mission selection logic...
6 };
7
8 return (
9 <div className="mission-control">
10 <h2>Available Missions</h2>
11 <ul>
12 {missions.map(mission => (
13 <li key={mission.id}>
14 {mission.name}
15 <button onClick={(e) => selectMission(mission.id, e)}>
16 Select
17 </button>
18 </li>
19 ))}
20 </ul>
21 </div>
22 );
23}2. Using bind
1function MissionControl({ missions }) {
2 const selectMission = (missionId, event) => {
3 console.log(`Selected mission with ID: ${missionId}`);
4 // Mission selection logic...
5 };
6
7 return (
8 <div className="mission-control">
9 <h2>Available Missions</h2>
10 <ul>
11 {missions.map(mission => (
12 <li key={mission.id}>
13 {mission.name}
14 <button onClick={selectMission.bind(null, mission.id)}>
15 Select
16 </button>
17 </li>
18 ))}
19 </ul>
20 </div>
21 );
22}The arrow function approach is more common and usually more readable.
Popular events in React
React supports a wide range of events, which can be divided into several categories:
Mouse events
onClick- clickonDoubleClick- double clickonMouseEnter- cursor enters an elementonMouseLeave- cursor leaves an elementonMouseMove- cursor moves over an element
Form events
onChange- change in a form element's valueonSubmit- form submissiononFocus- element receives focusonBlur- element loses focus
Keyboard events
onKeyDown- key pressed downonKeyPress- deprecated, useonKeyDowninsteadonKeyUp- key released
Touch events (for mobile devices)
onTouchStart- touch beginsonTouchMove- movement during touchonTouchEnd- touch ends
Examples of handling different events
Mouse events with state
State remembers whether the cursor is over the panel and whether the panel has been clicked, and the onMouseEnter, onMouseLeave and onClick events only change that state:
1function InteractivePanel() {
2 const [isHovered, setIsHovered] = useState(false);
3 const [isClicked, setIsClicked] = useState(false);
4
5 return (
6 <div
7 className={`panel ${isHovered ? 'hovered' : ''} ${isClicked ? 'clicked' : ''}`}
8 onMouseEnter={() => setIsHovered(true)}
9 onMouseLeave={() => {
10 setIsHovered(false);
11 setIsClicked(false);
12 }}
13 onClick={() => setIsClicked(!isClicked)}
14 >
15 <h3>Interactive Panel</h3>
16 <p>
17 {isHovered ? 'Cursor over panel' : 'Hover over the panel'}
18 {isClicked ? ' - Panel active' : ' - Click to activate'}
19 </p>
20 </div>
21 );
22}Keyboard events
Keys are handled just like clicks - with onKeyDown on an element, and event.key tells you which key was pressed:
1function SpaceshipControls() {
2 const [position, setPosition] = useState({ x: 50, y: 50 });
3
4 const handleKeyDown = (event) => {
5 switch(event.key) {
6 case 'ArrowUp':
7 setPosition(prev => ({ ...prev, y: Math.max(0, prev.y - 5) }));
8 break;
9 case 'ArrowDown':
10 setPosition(prev => ({ ...prev, y: Math.min(100, prev.y + 5) }));
11 break;
12 case 'ArrowLeft':
13 setPosition(prev => ({ ...prev, x: Math.max(0, prev.x - 5) }));
14 break;
15 case 'ArrowRight':
16 setPosition(prev => ({ ...prev, x: Math.min(100, prev.x + 5) }));
17 break;
18 default:
19 return;
20 }
21 };
22
23 return (
24 <div className="spaceship-simulator" tabIndex={0} onKeyDown={handleKeyDown}>
25 <h2>Spaceship Simulator</h2>
26 <p>Click the simulator and steer the ship with the arrow keys</p>
27
28 <div className="universe" style={{ position: 'relative', width: '400px', height: '400px', background: 'black' }}>
29 <div
30 className="spaceship"
31 style={{
32 position: 'absolute',
33 left: `${position.x}%`,
34 top: `${position.y}%`,
35 transform: 'translate(-50%, -50%)',
36 width: '20px',
37 height: '20px',
38 background: 'red'
39 }}
40 ></div>
41 </div>
42
43 <div className="coordinates">
44 <p>Position X: {position.x}</p>
45 <p>Position Y: {position.y}</p>
46 </div>
47 </div>
48 );
49}tabIndex={0} lets the div element receive focus, so onKeyDown works once you click the simulator. Listening on the whole document requires useEffect with a cleanup - you will learn it in the module on effects.
Passing functions as props
We often need to pass event handler functions from a parent component to a child component. This is a common practice that enables communication between components:
1function MissionControl({ onLaunch, onAbort }) {
2 const [countdown, setCountdown] = useState(10);
3 const [isLaunching, setIsLaunching] = useState(false);
4 // useRef keeps the timer id between renders (changing a ref does not trigger a re-render)
5 const timerRef = useRef(null);
6
7 const stopTimer = () => {
8 clearInterval(timerRef.current);
9 timerRef.current = null;
10 };
11
12 const startCountdown = () => {
13 setIsLaunching(true);
14 let secondsLeft = 10;
15 setCountdown(secondsLeft);
16
17 timerRef.current = setInterval(() => {
18 secondsLeft -= 1;
19 setCountdown(secondsLeft);
20 if (secondsLeft === 0) {
21 stopTimer();
22 setIsLaunching(false);
23 onLaunch(); // Calling the function from props after the countdown finishes (outside the state updater function)
24 }
25 }, 1000);
26 };
27
28 const abortLaunch = () => {
29 stopTimer(); // Stop the countdown first
30 setIsLaunching(false);
31 setCountdown(10);
32 onAbort(); // Calling the function from props on abort
33 };
34
35 return (
36 <div className="mission-control">
37 <h2>Mission Control Center</h2>
38 <div className="countdown">
39 {isLaunching
40 ? `Launch in: ${countdown}`
41 : 'Ready for launch'}
42 </div>
43
44 <div className="control-buttons">
45 {!isLaunching ? (
46 <button onClick={startCountdown}>Start Countdown</button>
47 ) : (
48 <button onClick={abortLaunch} className="abort">Abort Launch</button>
49 )}
50 </div>
51 </div>
52 );
53}
54
55// Using the component
56function SpaceProgram() {
57 const handleLaunch = () => {
58 console.log('Rocket has launched!');
59 // Post-launch logic...
60 };
61
62 const handleAbort = () => {
63 console.log('Launch aborted!');
64 // Post-abort logic...
65 };
66
67 return (
68 <div className="space-program">
69 <h1>Space Program</h1>
70 <MissionControl
71 onLaunch={handleLaunch}
72 onAbort={handleAbort}
73 />
74 </div>
75 );
76}The function passed to setState (a functional update) must be pure: in development mode (StrictMode) React calls it twice, so calling onLaunch() inside it would launch the rocket twice. That is why we count the seconds in a plain secondsLeft variable and call onLaunch() and onAbort() directly in the timer and handler code. The timer id is kept in useRef so that abortLaunch can stop it - you will learn more about useRef with uncontrolled components in the lesson on data flow.
Form handling
Forms are a key element of user interaction in web applications. React offers two approaches to form handling: controlled and uncontrolled components.
Controlled forms
In controlled components, form data is controlled by React state:
1function MissionSetup() {
2 const [missionData, setMissionData] = useState({
3 name: '',
4 destination: 'Moon',
5 crew: 3,
6 duration: 7
7 });
8
9 const handleChange = (e) => {
10 const { name, value } = e.target;
11 setMissionData({
12 ...missionData,
13 [name]: value
14 });
15 };
16
17 const handleSubmit = (e) => {
18 e.preventDefault();
19 console.log('Mission data:', missionData);
20 // Data submission logic...
21 };
22
23 return (
24 <div className="mission-setup">
25 <h2>Mission Configuration</h2>
26
27 <form onSubmit={handleSubmit}>
28 <div className="form-group">
29 <label htmlFor="name">Mission Name:</label>
30 <input
31 type="text"
32 id="name"
33 name="name"
34 value={missionData.name}
35 onChange={handleChange}
36 required
37 />
38 </div>
39
40 <div className="form-group">
41 <label htmlFor="destination">Destination:</label>
42 <select
43 id="destination"
44 name="destination"
45 value={missionData.destination}
46 onChange={handleChange}
47 >
48 <option value="Moon">Moon</option>
49 <option value="Mars">Mars</option>
50 <option value="ISS">International Space Station</option>
51 <option value="Asteroid">Asteroid Belt</option>
52 </select>
53 </div>
54
55 <div className="form-group">
56 <label htmlFor="crew">Number of Crew Members:</label>
57 <input
58 type="number"
59 id="crew"
60 name="crew"
61 min="1"
62 max="10"
63 value={missionData.crew}
64 onChange={handleChange}
65 />
66 </div>
67
68 <div className="form-group">
69 <label htmlFor="duration">Duration (days):</label>
70 <input
71 type="number"
72 id="duration"
73 name="duration"
74 min="1"
75 value={missionData.duration}
76 onChange={handleChange}
77 />
78 </div>
79
80 <button type="submit">Plan Mission</button>
81 </form>
82 </div>
83 );
84}In the example above:
- Each form field has a corresponding entry in the state
- The value of each field is controlled by state
- The
onChangeevent updates the state, which in turn updates the field value - The
onSubmitevent is used to handle form submission
Handling multiple form field types
Different form fields require slightly different approaches:
1function AstronautRegistration() {
2 const [formData, setFormData] = useState({
3 name: '',
4 age: '',
5 specialty: 'pilot',
6 experience: '',
7 isTrainee: false,
8 bio: '',
9 preferredShift: 'morning'
10 });
11
12 const handleChange = (e) => {
13 const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
14 setFormData({
15 ...formData,
16 [e.target.name]: value
17 });
18 };
19
20 const handleSubmit = (e) => {
21 e.preventDefault();
22 console.log('Astronaut data:', formData);
23 // Registration logic...
24 };
25
26 return (
27 <div className="astronaut-registration">
28 <h2>Astronaut Registration</h2>
29
30 <form onSubmit={handleSubmit}>
31 {/* Text input */}
32 <div className="form-group">
33 <label htmlFor="name">Full Name:</label>
34 <input
35 type="text"
36 id="name"
37 name="name"
38 value={formData.name}
39 onChange={handleChange}
40 required
41 />
42 </div>
43
44 {/* Number input */}
45 <div className="form-group">
46 <label htmlFor="age">Age:</label>
47 <input
48 type="number"
49 id="age"
50 name="age"
51 value={formData.age}
52 onChange={handleChange}
53 required
54 />
55 </div>
56
57 {/* Select */}
58 <div className="form-group">
59 <label htmlFor="specialty">Specialty:</label>
60 <select
61 id="specialty"
62 name="specialty"
63 value={formData.specialty}
64 onChange={handleChange}
65 >
66 <option value="pilot">Pilot</option>
67 <option value="engineer">Engineer</option>
68 <option value="scientist">Scientist</option>
69 <option value="doctor">Doctor</option>
70 </select>
71 </div>
72
73 {/* Radio buttons */}
74 <div className="form-group">
75 <p>Preferred Shift:</p>
76 <label>
77 <input
78 type="radio"
79 name="preferredShift"
80 value="morning"
81 checked={formData.preferredShift === 'morning'}
82 onChange={handleChange}
83 />
84 Morning
85 </label>
86 <label>
87 <input
88 type="radio"
89 name="preferredShift"
90 value="evening"
91 checked={formData.preferredShift === 'evening'}
92 onChange={handleChange}
93 />
94 Evening
95 </label>
96 </div>
97
98 {/* Checkbox */}
99 <div className="form-group">
100 <label>
101 <input
102 type="checkbox"
103 name="isTrainee"
104 checked={formData.isTrainee}
105 onChange={handleChange}
106 />
107 Trainee Status
108 </label>
109 </div>
110
111 {/* Textarea */}
112 <div className="form-group">
113 <label htmlFor="bio">Biography:</label>
114 <textarea
115 id="bio"
116 name="bio"
117 value={formData.bio}
118 onChange={handleChange}
119 rows="4"
120 ></textarea>
121 </div>
122
123 {/* Range */}
124 <div className="form-group">
125 <label htmlFor="experience">Years of Experience: {formData.experience}</label>
126 <input
127 type="range"
128 id="experience"
129 name="experience"
130 min="0"
131 max="30"
132 value={formData.experience}
133 onChange={handleChange}
134 />
135 </div>
136
137 <button type="submit">Register</button>
138 </form>
139 </div>
140 );
141}Error handling and form validation
In real-world applications, it is important to validate user input:
1function MissionPlanner() {
2 const [formData, setFormData] = useState({
3 missionName: '',
4 launchDate: '',
5 budget: ''
6 });
7
8 const [errors, setErrors] = useState({});
9
10 const validateForm = () => {
11 const newErrors = {};
12
13 // Mission name validation
14 if (!formData.missionName.trim()) {
15 newErrors.missionName = 'Mission name is required';
16 } else if (formData.missionName.length < 3) {
17 newErrors.missionName = 'Mission name must be at least 3 characters';
18 }
19
20 // Launch date validation
21 if (!formData.launchDate) {
22 newErrors.launchDate = 'Launch date is required';
23 } else {
24 const selectedDate = new Date(formData.launchDate);
25 const today = new Date();
26
27 if (selectedDate <= today) {
28 newErrors.launchDate = 'Launch date must be in the future';
29 }
30 }
31
32 // Budget validation
33 if (!formData.budget) {
34 newErrors.budget = 'Budget is required';
35 } else if (isNaN(formData.budget) || Number(formData.budget) <= 0) {
36 newErrors.budget = 'Budget must be a positive number';
37 }
38
39 setErrors(newErrors);
40 return Object.keys(newErrors).length === 0; // Form is valid if there are no errors
41 };
42
43 const handleChange = (e) => {
44 const { name, value } = e.target;
45 setFormData({
46 ...formData,
47 [name]: value
48 });
49
50 // Clear error for the field being changed
51 if (errors[name]) {
52 setErrors({
53 ...errors,
54 [name]: undefined
55 });
56 }
57 };
58
59 const handleSubmit = (e) => {
60 e.preventDefault();
61
62 if (validateForm()) {
63 console.log('Mission data submitted:', formData);
64 // Data submission logic...
65 } else {
66 console.log('Form contains errors');
67 }
68 };
69
70 return (
71 <div className="mission-planner">
72 <h2>Space Mission Planning</h2>
73
74 <form onSubmit={handleSubmit}>
75 <div className="form-group">
76 <label htmlFor="missionName">Mission Name:</label>
77 <input
78 type="text"
79 id="missionName"
80 name="missionName"
81 value={formData.missionName}
82 onChange={handleChange}
83 className={errors.missionName ? 'error' : ''}
84 />
85 {errors.missionName && <div className="error-message">{errors.missionName}</div>}
86 </div>
87
88 <div className="form-group">
89 <label htmlFor="launchDate">Launch Date:</label>
90 <input
91 type="date"
92 id="launchDate"
93 name="launchDate"
94 value={formData.launchDate}
95 onChange={handleChange}
96 className={errors.launchDate ? 'error' : ''}
97 />
98 {errors.launchDate && <div className="error-message">{errors.launchDate}</div>}
99 </div>
100
101 <div className="form-group">
102 <label htmlFor="budget">Budget (in millions $):</label>
103 <input
104 type="number"
105 id="budget"
106 name="budget"
107 value={formData.budget}
108 onChange={handleChange}
109 className={errors.budget ? 'error' : ''}
110 />
111 {errors.budget && <div className="error-message">{errors.budget}</div>}
112 </div>
113
114 <button type="submit">Plan Mission</button>
115 </form>
116 </div>
117 );
118}Summary
Event handling is a key element of creating interactive applications in React. Thanks to it, we can:
- Respond to user actions such as clicks, mouse movement, or text input
- Update component state based on interactions
- Communicate between components by passing functions as props
- Handle forms and validate user input
Proper event handling, combined with state management, allows you to create dynamic, responsive user interfaces that provide users with an intuitive experience.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3function EventDemo() {
4 const [clickCount, setClickCount] = useState(0);
5 const [inputText, setInputText] = useState('');
6 const [isHovered, setIsHovered] = useState(false);
7 const [selectedOption, setSelectedOption] = useState('Mars');
8
9 const handleButtonClick = () => {
10 setClickCount(clickCount + 1);
11 };
12
13 const handleInputChange = (event) => {
14 setInputText(event.target.value);
15 };
16
17 const handleSubmit = (event) => {
18 event.preventDefault();
19 alert(`Form submitted with text: "${inputText}"`);
20 };
21
22 const handleKeyDown = (event) => {
23 if (event.key === 'Enter') {
24 alert('Enter pressed!');
25 }
26 };
27
28 return (
29 <div className="event-demo">
30 <h1>React Event Handling Demo</h1>
31
32 <div className="demo-section">
33 <h2>Mouse Events</h2>
34 <button
35 onClick={handleButtonClick}
36 onMouseEnter={() => setIsHovered(true)}
37 onMouseLeave={() => setIsHovered(false)}
38 className={isHovered ? 'hovered' : ''}
39 >
40 Click me! (Clicked: {clickCount} times)
41 </button>
42 </div>
43
44 <div className="demo-section">
45 <h2>Form Events</h2>
46 <form onSubmit={handleSubmit}>
47 <input
48 type="text"
49 value={inputText}
50 onChange={handleInputChange}
51 onKeyDown={handleKeyDown}
52 placeholder="Type text and press Enter or submit the form"
53 />
54 <select
55 value={selectedOption}
56 onChange={(e) => setSelectedOption(e.target.value)}
57 >
58 <option value="Mars">Mars</option>
59 <option value="Moon">Moon</option>
60 <option value="Venus">Venus</option>
61 <option value="Jupiter">Jupiter</option>
62 </select>
63 <button type="submit">Submit form</button>
64 </form>
65
66 <div className="output">
67 <p>Current text: {inputText}</p>
68 <p>Selected option: {selectedOption}</p>
69 </div>
70 </div>
71
72 <div className="demo-section">
73 <h2>Touch and Interaction Events</h2>
74 <div
75 className="interactive-area"
76 onTouchStart={() => console.log('Touch started')}
77 onTouchEnd={() => console.log('Touch ended')}
78 onDoubleClick={() => alert('Double click!')}
79 >
80 Double-click or tap (check the console)
81 </div>
82 </div>
83 </div>
84 );
85}
86
87function App() {
88 return (
89 <div className="app">
90 <EventDemo />
91 </div>
92 );
93}
94
95export default App;Spotted a mistake in this lesson?
Hands-on tasks in the game
- Horizontal ordering
Arrange the correct syntax for updating state using the previous value:
- Code editor
Create an oxygen counter: const [oxygen, setOxygen] = useState(21). Display the text O2 Level: {oxygen}% and add two buttons: the + button raises the level by 1 (<button onClick={() => setOxygen(prev => prev + 1)}>+</button>) and the - button lowers it by 1. The level must stay within 0-100.
- Click in order
Build the correct click handler attribute in JSX:
- Click in order
Arrange the process of using useState: