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

Event Handling

11 min read
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:

  1. Event names use camelCase convention instead of lowercase:

    • HTML: onclick, onmouseover
    • React: onClick, onMouseOver
  2. 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>
  3. 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>

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.

React supports a wide range of events, which can be divided into several categories:

Mouse events

  • onClick - click
  • onDoubleClick - double click
  • onMouseEnter - cursor enters an element
  • onMouseLeave - cursor leaves an element
  • onMouseMove - cursor moves over an element

Form events

  • onChange - change in a form element's value
  • onSubmit - form submission
  • onFocus - element receives focus
  • onBlur - element loses focus

Keyboard events

  • onKeyDown - key pressed down
  • onKeyPress - deprecated, use onKeyDown instead
  • onKeyUp - key released

Touch events (for mobile devices)

  • onTouchStart - touch begins
  • onTouchMove - movement during touch
  • onTouchEnd - 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:

  1. Each form field has a corresponding entry in the state
  2. The value of each field is controlled by state
  3. The onChange event updates the state, which in turn updates the field value
  4. The onSubmit event 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:

  1. Respond to user actions such as clicks, mouse movement, or text input
  2. Update component state based on interactions
  3. Communicate between components by passing functions as props
  4. 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:

Useful articles