Using the React useState hook for forms

Published January 4, 2022

The React useState hook can be used many times to manage as many state variables as necessary in a given component. This is especially useful for forms, which often have several fields that need to be tracked in a component's local state.

Manage form state independently

Let's take a look at an example of a basic form with two fields, an email and a password field. In this example, we will manage the email and password states separately as individual state variables.

import React, { useState } from "react";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = (e) => {
    // prevent page reload submit behavior
    e.preventDefault();

    console.log(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        placeholder="Email"
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        name="password"
        type="password"
        placeholder="Password"
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Each field has its own setter function and each field has its own state reference. The email field is referenced by email and is updated by setEmail. The password field is referenced by password and is updated by setPassword.

Manage form state in one object

We can also use useState to manage form state within one single object. This approach only needs one useState call. Rather than email and password being managed by separate state variables, they become properties of one state object.

The tricky part with this approach is properly updating the state, which is now an object.

When using useState with an object, we must remember to spread in the previous state when performing updates to this object. This allows the object to be updated properly without any unexpected results. It is not done automatically for us. Therefore, we must be sure to manually merge the previous state object's properties with the new state changes.

import React, { useState } from "react";

const Login = () => {
  const [state, setState] = useState({
    email: '',
    password: ''
  });

  const handleChange = (e) => {
    setState({
      // spread in the previous state
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    // prevent page reload submit behavior
    e.preventDefault();

    console.log(state);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        placeholder="Email"
        onChange={handleChange}
      />
      <input
        name="password"
        type="password"
        placeholder="Password"
        onChange={handleChange}
      />
      <button type="submit">Submit</button>
    </form>
  );
}
export default Login;

We could have also used an updater function for our setState setter function.

const handleChange = (e) => {  
  setState((prevState) => ({
    ...prevState,
    [e.target.name]: e.target.value,
  }));
};

Though it's a bit more verbose, it's best to go with this updater function approach when the next state of a variable depends on its previous state.

Conclusion

We've looked at two different approaches to effectively using useState for managing form state in React. If your form consists of many fields, consider managing the form's state in one object. If your form consists of just a few fields, managing form state as independent fields should be fine.