When creating a form with React components, it is common to use an onChange
handler to listen for changes to input elements and record their values in state. Besides handling just one input, a single onChange
handler can be set up to handle many different inputs in the form.
The onChange
handler will listen for any change to the input and fire an event when the value changes. With a text input field like this, we can pass the onChange
prop:
1<label>
2 First name
3 <input
4 type="text"
5 onChange={handleChange}
6 />
7</label>
The value of the prop is the handleChange function; It is an event handler. The event that the handler receives as a parameter is an object that contains a target
field. This target
is the DOM element that the event handler is bound to (ie, the text input field). By accessing this field, we can determine what the target
's value is changed to:
1function handleChange(evt) {
2 console.log("new value", evt.target.value);
3}
This handler will simply print the new value that the text input is changed to.
But this change handler, so far, is rather useless. Instead, we can imagine that the change handler should listen for changes and save the new changes in internal state for later form submission.
In order to accomplish this, it is common to set up the input field as a controlled component, meaning that React state drives its value in the UI. To do this, we'll add some React state and set the input field's value
prop with it:
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 firstName: ""
5 })
6 return (
7 <form>
8 <label>
9 First name
10 <input
11 type="text"
12 value={state.firstName}
13 onChange={handleChange}
14 />
15 </label>
16 </form>
17 );
18}
Then we'll make the handleChange
function update the state with the setState
function:
1function handleChange(evt) {
2 setState({ firstName: evt.target.value });
3}
Now we have a controlled component that captures changes and updates state accordingly. This is a working example of a single input.
What if we add another input to the mix? Instead of just a first name field, we add a last name field as a second text input field:
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 firstName: "",
5 lastName: ""
6 })
7 return (
8 <form>
9 <label>
10 First name
11 <input
12 type="text"
13 name="firstName"
14 value={state.firstName}
15 onChange={handleChange}
16 />
17 </label>
18 <label>
19 Last name
20 <input
21 type="text"
22 name="lastName"
23 value={state.lastName}
24 onChange={handleChange}
25 />
26 </label>
27 </form>
28 );
29}
There are a couple of significant changes that have been made, in addition to the new input field. A new lastName
string has been added to state
to store the data from this input, and each of the input elements have a new name
prop. These name
props will show up in the DOM as a name
attributes on the input HTML elements. We'll consume them in an adjustment to the handler code:
1function handleChange(evt) {
2 const value = evt.target.value;
3 setState({
4 ...state,
5 [evt.target.name]: value
6 });
7}
In addition to getting the value
from the event target, we get the name
of that target as well. This is the essential point for handling multiple input fields with one handler. We funnel all changes through that one handler but then distinguish which input the change is coming from using the name
.
This example is using [evt.target.name]
, with the name in square brackets, to create a dynamic key name in the object. Because the form name
props match the state
property keys, the firstName
input will set the firstName
state and the lastName
input will separately set the lastName
state.
Also note that, because we are using a single state
object that contains multiple properties, we're spreading (...state
) the existing state back into the new state value, merging it manually, when calling setState
. This is required when using React.useState
in the solution.
React normalizes the use of many other types of input fields so that they are consumed in a very similar way. But there are a few differences that we will cover here. Instead of rewriting the entire example each time, we will see just the subset of input elements and state that relate to the input type being discussed.
textarea
inputs functions exactly as <input type="text" /> does and
handleChange` remains unchanged:
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 bio: ""
5 })
6 return (
7 <form>
8 <label>
9 Bio
10 <textarea name="bio" value={state.bio} onChange={handleChange} />
11 </label>
12 </form>
13 );
14}
The select
element, beyond its DOM structure being defined slightly different, is the same in the way it publishes changes and consumes values:
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 version: "16.8"
5 })
6 return (
7 <form>
8 <label>
9 Favorite version
10 <select name="version" onChange={handleChange} value={state.version}>
11 <option value="16.8">v16.8.0</option>
12 <option value="16.7">v16.7.0</option>
13 <option value="16.6">v16.6.0</option>
14 <option value="16.5">v16.5.0</option>
15 </select>
16 </label>
17 </form>
18 );
19}
Note that the currently-selected value is set via the value
prop on the parent select
element. The initially-selected value is set by the initial state.version
value. value
props on the child option
elements are the potential values to be changed to.
The <input type="radio" />
functions a little differently than other inputs. Its value
prop is static, representing the option to select. The name
is duplicated and must match across the radio buttons that make up the radio button group. The checked
prop is introduced with a condition that determines whether that particular button is shown as active or not.
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 level: "master"
5 })
6 return (
7 <form>
8 <div>
9 Level
10 <label>
11 Acolyte
12 <input
13 type="radio"
14 name="level"
15 value="acolyte"
16 checked={state.level === "acolyte"}
17 onChange={handleChange}
18 />
19 </label>
20 <label>
21 Master
22 <input
23 type="radio"
24 name="level"
25 value="master"
26 checked={state.level === "master"}
27 onChange={handleChange}
28 />
29 </label>
30 </div>
31 </form>
32 );
33}
The <input type="checkbox" />
element will look a bit like the radio button, in that it utilizes the checked
prop. But checkboxes are independent controls, not existing in a group, thus they have unique name
props like most other inputs:
1import React from "react";
2function Form() {
3 const [state, setState] = React.useState({
4 hooks: true
5 })
6 return (
7 <form>
8 <label>
9 With hooks
10 <input
11 type="checkbox"
12 name="hooks"
13 checked={state.hooks}
14 onChange={handleChange}
15 />
16 </label>
17 </form>
18 );
19}
There is an important change necessitated with checkboxes, and that is an adjustment to the handleChange
function:
1function handleChange(evt) {
2 const value =
3 evt.target.type === "checkbox" ? evt.target.checked : evt.target.value;
4 setState({
5 ...state,
6 [evt.target.name]: value
7 });
8}
The important change here is in the determination of the value
. A checkbox doesn't contain a string value like text inputs or text areas, and it doesn't contain static options like selects or radio buttons. Instead, it simply knows whether it's on or off. Thus we have to extract its value in a one-off kind of way, checking for the evt.target.type
to equal "checkbox"
and then looking to evt.target.checked
for the value. Note also that the checked
prop is a boolean and, thus, the hooks
property in state
is also of type boolean.
Now we have a knowledge of how to consume multiple inputs and the small differences between the common form fields that exist. We also know how to watch for changes across each of these input types and consume them in a single onChange
handler.
To handle this efficiently, we define each input with a name
prop. This matches a corresponding field in React state. In order to update that state, we use the change event’s evt.target.name
field.
To see all of these inputs together in a working example, run this jaketrent/demo-single-change-handler repo on Github.
Explore these React courses from Pluralsight to continue learning!