[React] Manage Multiple Form Inputs with Single State Variable

Utilizing ES6 destructuring and dynamic object keys.

Reference: John Smilga and freeCodeCamp's Full React Course 2020

When creating an application with few inputs, setting separate state variables for each input field is not an issue, as seen below:

import React, { useState } from 'react'

const ControlledInputs = () => {
// Separate state variables (firstName, email) to store input.
  const [firstName, setFirstName] = useState("");
  const [email, setEmail] = useState("");
  const [people, setPeople] = useState([]);

  const handleSubmit = (event) => {
      // prevents page from refreshing after submitting the form
      event.preventDefault();
      // if 'firstName' and 'email' aren't empty strings (falsy values) 
      if (firstName && email) {
        // create an object 'person', which stores the 'firstName' and 'email' values
        // to properties with the same name (ES6 syntax)
        const person = { id: new Date().getTime().toString(), firstName, email };
        // update the 'people' list by appending the newly created 'person' object.
        setPeople((people) => {
          return [...people, person]; // uses ES6 spread (...) operator
        });
        // set 'firstName' and 'email' back to empty strings.
        setFirstName("");
        setEmail("");
      } else {
        // if either 'firstName' or 'email' is an empty string, log message
        console.log("empty values");
      }
    };

  return (
    <article>
      <form className="form">
        <div className="form-control">
          <label htmlFor="firstName">Name: </label>
          <input
            type="text"
            id="firstName"
            name="firstName"
            value={firstName}
            onChange={(e) => setFirstName(e.target.value)}
            autoComplete="off"
          />
        </div>
        <div className="form-control">
          <label htmlFor="email">Email: </label>
          <input
            type="text"
            id="email"
            name="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            autoComplete="off"
          />
        </div>
        <button type="submit">add person</button>
      </form>
    </article>
  );
};

However, when there are multiple input variables, it is inconvenient to have a corresponding state variable for each and every input field, because it is harder to maintain the code.

For example, when an input needs to be added or removed, another state variable must be set or deleted.

On the other hand, if you set a single state variable that holds all input values, you only need to add or remove necessary input fields without having to change the state variables. You just need to connect the value and onChange attribute of the <input> element to the state variable.

Using Objects for State Variables

We can easily do this by setting an object that holds all the input values to a state variable:

const [person, setPerson] = useState({ firstName: "", email: "", age: ""});

Then, we can connect each <input>'s value attribute to the person state variable, as seen below:

<div className="form-control">
  <label htmlFor="firstName">Name : </label>
  <input
      type="text"
      id="firstName"
      name="firstName"
      value={person.firstName} // value is set to the corresponding key of the 'person' state value.
      onChange={handleChange}
  />
</div>
<div className="form-control">
  <label htmlFor="email">Email : </label>
  <input
      type="text"
      id="email"
      name="email"
      value={person.email}
      onChange={handleChange}
  />
</div>
<div className="form-control">
  <label htmlFor="age">Age : </label>
  <input
      type="text"
      id="age"
      name="age"
      value={person.age}
      onChange={handleChange}
  />
</div>

Using ES6 Destructuring and Dynamic Object Keys to Update State

Now we can define the handleChange method, which we connect to the onChange attribute of the <input>, the redefine the handleSubmit method.

handleChange()

/*
*/
const handleChange = (e) => {
    const name = e.target.name; // takes the 'name' attribute's value of the input field being triggered.
    const value = e.target.value; // takes the 'value' of the 'event.target', which is the input being generated by the user.
    setPerson({ ...person, [name]: value }); // destructures the 'person' object, then allows property with key corresponding to 'name' have a value corresponding to 'value' assigned.
  };

handleSubmit()

const handleSubmit = (e) => {
    e.preventDefault();
    if (person.firstName && person.email && person.age) {
      const newPerson = { ...person, id: new Date().getTime().toString() };
      setPeople([...people, newPerson]);
      setPerson({ firstName: "", email: "", age: "" });
    }
  };

Key Takeaways

  1. Create one state variable that manages all input data by using an object literal.
  2. Use dot notation to access desired properties when updating specific input values.
  3. Use ES6 destructuring and dynamic object property keys to update the state variable.