DEV Community

Martin Souza
Martin Souza

Posted on

Single-Handedly Handling React Forms

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>
)
Enter fullscreen mode Exit fullscreen mode

...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
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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)