Like most other things it offers, React provides a streamlined way to implement forms. Not only does the declarative syntax of JSX make building forms easier, but by leveraging state in combination with form elements, React allows us to control our forms' inputs. This enables easy collection of input data as well as other useful wizardry like input validation.
However, in learning how to make controlled forms with React, I found there's a sizable leap from the basics of setting up form inputs, each connected to its own independent state variable and each with a separate handler function, to the much more elegant and slick technique of using a single state variable and one handler function to manage an entire form. In this post, I'll walk through how to set up that trick.
Note: This post assumes familiarity with the concept of controlled inputs and how to build a basic form in React.
For a very small form with just a couple of inputs, having a separate state variable and change handler function for each input isn't that much trouble. The more inputs you have, though, the more unwieldy your code becomes—and fast. If each input in our form component is set up like this:
const [formNameInput, setFormNameInput] = useState("")
// ...
const handleNameInput = (e) => {
setFormNameInput(e.target.value)
}
// ...
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formNameInput}
onChange={handleNameInput}
/>
// ...
</form>
)
...We'll accumulate a large collection of state variables and state setter functions. We could cut some lines of code by invoking each input's respective setter function in-line inside the onChange
, rather than writing individual handler functions, but even so the code for managing our form data will rapidly get out of hand. We can avoid this entire headache by using a single state variable to hold all our form data and one handler function that can tell which input it's being called from, and can accordingly update the right part of the form data in state.
Objectify Your Data
The key to this (pun intended) is to store all our form data in an object, rather than in separate pieces. Let's say our form has three inputs: name, description, and number. We can initialize one state variable and keep all three pieces of data there inside an object:
const [formData, setFormData] = useState({
name: "",
description: "",
number: 0
})
When we invoke useState
, we set its initial value to an object with keys that match our form's inputs (more on that later), and assign each key a default value (empty strings for name and description, and 0 for number).
As an aside, we could save this object to a separate variable, and use that to set our initial state:
const initialFormState = {
name: "",
description: "",
number: 0
}
const [formData, setFormData] = useState(initialFormState)
This isn't necessary, but it might look a little neater. It also comes in handy for resetting the form later.
Input Names are Valuable
The second key part (strained pun still intended) of our setup is giving each of our form inputs a name
attribute that exactly matches the corresponding key in the formData
object:
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
name="name"
onChange={handleInput}
/>
<input
type="textarea"
value={formData.description}
name="description"
onChange={handleInput}
/>
<input
type="number"
value={formData.number}
name="number"
onChange={handleInput}
/>
<input type="submit" />
</form>
)
This is essential! Each input element must have a name
attribute with a value exactly the same as one of the keys in the formData
object. These matching name
attributes are how our single handler function will be able to know which key/value pair to update in formData
.
Note also that we're now setting the value
attribute of each input with a reference to the corresponding key in the formData
object.
Handling It All
With our formData
object in a single state variable and our controlled inputs given matching name
attributes, we can finally write our handler function:
const handleInput = (e) => {
const nameInput = e.target.name
const valueInput = e.target.value
setFormData({
...formData,
[nameInput]: valueInput
})
}
Let's break down what's going on here. First of all, notice we're still passing in the event object from whichever onChange
invokes the function. Then we pull two pieces of information out of that event object, and assign them to variables: the name
attribute of the input element from which handleInput
was invoked, and the value the user has just entered into that input, which triggered the onChange
.
Next, we invoke our state setter function setFormData
so we can update state with the newly entered information. Since our state variable formData
contains an object, we have to replace the whole object when we use the setter function to update it. Thus, we use the spread operator ...
to copy the entire current contents of formData
, then specify which key/value pair to change in that copy using our variables nameInput
and valueInput
.
Note the square brackets around nameInput
. This is just the syntax required to interpolate a variable for the name of an object's key in this case. This detail tripped me up when I first learned to write this kind of handler function.
Because we're pulling the name
attribute from the input element, and because we made sure that name
exactly matches a key in the formData
object, our handler function can use the name
attribute to specify which key/value pair to update with the state setter function. The other key/value pairs will remain unchanged—so whichever input the user interacts with, only that input's corresponding information in formData
will change. Our one function can handle every input element in our form, no matter how many we add.
Finally, a further benefit of managing forms this way is that all your form information will already be packaged in a nice, neat object, ready to be taken in and passed on by your submit handler function. This obviates the work of building such an object from the data in separate state variables for each input element.
Smoothing Things Out
We can refactor our single handler function to make it more compact. For one thing we could use a little object destructuring to simplify the variable assignment part:
const handleInput = (e) => {
const {name, value} = e.target
setFormData({
...formData,
[name]: value
})
}
Here we're just taking e.target.name
and e.target.value
and assigning them to variables also called name
and value
, then using those in the state setter function as before.
However, we can still do one better, and eliminate the variable assignment entirely:
const handleInput = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
Turns out we don't even need those helper variables! Our handler function can just invoke the state setter function and directly use the name
attribute and input value from the event object.
React makes forms pretty easy, but there's definitely a pronounced learning curve to go from the basic idea of connecting each input element to its own state to managing a whole form with one piece of state and one handler function, no matter how many inputs it has. However, not only is it worth doing so simply to make your code less cumbersome, but ultimately this approach is all but necessary to deal with forms with more than a tiny number of inputs.
Hopefully walking through each step of how to set up a React form like this helps you make that level-up.
Top comments (0)