Form Handling in React

In this lesson, we are going to discuss how to use states and event handlers to collect user input via forms.

So far, we've used onClick as a simple example to demonstrate event handling. However, handling forms requires more than just onClick. To manage form interactions, we need to understand two key events: onChange and onSubmit.

Use onChange to access user input

The onChange event will be fired when the user types inside a input field, the associated event object will pass the user input to the event handler function, which can be accessed like this:

jsx
1export default function App() {
2  return (
3    <input
4      type="text"
5      onChange={(e) => {
6        console.log(e.target.value);
7      }}
8    />
9  );
10}

e.target.value returns the current user input, and the above example will output whatever the user types to the console.

In practice, we often use states to capture and store user inputs. For example, instead of logging to the console, setText(e.target.value) will use the user input to update the state text.

jsx
1import { useState } from "react";
2
3export default function App() {
4  const [text, setText] = useState("");
5  return (
6    <>
7      <p>Input: {text}</p>
8      <input type="text" onChange={(e) => setText(e.target.value)} />
9    </>
10  );
11}

This technique applies to most other input fields, such as password, email, number, radio, date, time, range, and color. For all of these input fields, the user input can be accessed via e.target.value.

The checkbox field

However, there are some exceptions we must discuss. For instance, for the checkbox input, you must use e.target.checked instead of e.target.value to access the user input.

jsx
1import { useState } from "react";
2
3export default function App() {
4  const [isChecked, setIsChecked] = useState(false);
5  return (
6    <>
7      <p>Checked: {isChecked.toString()}</p>
8      <input
9        type="checkbox"
10        checked={isChecked}
11        onChange={(e) => setIsChecked(e.target.checked)}
12      />
13    </>
14  );
15}

And remember that in this case, e.target.checked returns a Boolean value, so we need to use toString() to convert it into a string (line 7).

The selection field

For single selection field, things work as you would have expected. When the user selects an <option>, the associated value will be passed to the event handler via e.target.value.

jsx
1<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
2  <option value="apple">Apple</option>
3  <option value="banana">Banana</option>
4  <option value="orange">Orange</option>
5</select>

However, recall that it is possible to select multiple options by adding the multiple attribute. In this case, the selected options must be accessed using e.target.selectedOptions, which returns an HTMLCollection.

jsx
1<select
2  multiple={true}
3  onChange={(e) => {
4    console.log(e.target.selectedOptions);
5  }}>
6  <option value="apple">Apple</option>
7  <option value="banana">Banana</option>
8  <option value="orange">Orange</option>
9</select>

React selectedOptions

This collection is an array like structure, which can be converted into an regular array using the spread syntax [...e.target.selectedOptions]. You can then access the individual options using the map() method.

jsx
1<select
2  multiple={true}
3  value={selectedFruits}
4  onChange={(e) =>
5    setSelectedFruits(
6      [...e.target.selectedOptions].map((option) => option.value)
7    )
8  }>
9  <option value="apple">Apple</option>
10  <option value="banana">Banana</option>
11  <option value="orange">Orange</option>
12</select>

The file field

Similar to the selection field, the file input field allows the user to select single or multiple files. The selected file(s) can be accessed using either e.target.files[0] for single, or e.target.files for multiple.

jsx
1<input type="file" multiple onChange={(e) => setFile(e.target.files)} />
jsx
1<input type="file" onChange={(e) => setFile(e.target.files[0])} />

Use onSubmit to submit a form

The onSubmit event is fired when the entire form gets submitted. This is where you should wrap up all the user inputs, and send them to the backend to be processed.

We already discussed how to send HTTP requests using plain Javascript, and we are doing the same thing here, except you lose a bit of flexibility, and must follow the rules set by React. For example:

jsx
1import { useState } from "react";
2
3function ContactForm() {
4  const [name, setName] = useState("");
5  const [email, setEmail] = useState("");
6  const [message, setMessage] = useState("");
7
8  // Handle form submission
9  async function handleSubmit(e) {
10    e.preventDefault();
11    try {
12      const response = await fetch("/api/contact", {
13        method: "POST",
14        headers: {
15          "Content-Type": "application/json",
16        },
17        body: JSON.stringify(formData),
18      });
19      const result = await response.json();
20      console.log(result);
21    } catch (error) {
22      console.error("Error submitting form:", error);
23    }
24  }
25
26  return (
27    <form onSubmit={handleSubmit}>
28      <div>
29        <label>Name:</label>
30        <input
31          type="text"
32          value={name}
33          onChange={(e) => setName(e.target.value)}
34        />
35      </div>
36      <div>
37        <label>Email:</label>
38        <input
39          type="email"
40          value={email}
41          onChange={(e) => setEmail(e.target.value)}
42        />
43      </div>
44      <div>
45        <label>Message:</label>
46        <textarea
47          value={message}
48          onChange={(e) => setMessage(e.target.value)}
49        />
50      </div>
51      <button type="submit">Submit</button>
52    </form>
53  );
54}
55
56export default ContactForm;