DEV Community

Cover image for The Trap of the useState hook with forms
Ahmed Sarhan
Ahmed Sarhan

Posted on

The Trap of the useState hook with forms

A brief about the Series

In this Series of React What’s and Whatnots, we will discuss in a series of articles some of the patterns and mistakes that a lot of React developers - including myself - fall victim to.

A brief about the Article

In this Article we will discuss a common mistake related to handling forms state.

Project Setub

I have already prepared a Github repo for you to follow along with,
So let’s start by opening a terminal where you feel comfortable and run the following commands:

git clone https://github.com/AhmedSarhanSwvl/react-form.git 
cd react-form
npm install
npm run start
Enter fullscreen mode Exit fullscreen mode

Exploring our Application

Now we have our application up and running
As you can see, we have a simple five - six input form and a submit button.

Form UI
If we go back to the App.js file in our code, you will see this inside the jsx;

    <form>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input
            type="text"
            name="username"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="email">Email</label>
            <input
              type="email"
              name="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>
          <div className="form-group">
            <label htmlFor="phone">Phone</label>
            <input
              type="tel"
              name="phone"
              value={phone}
              onChange={(e) => setPhone(e.target.value)}
            />
          </div>
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="password">Password</label>
            <input
              type="password"
              name="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>
          <div className="form-group">
            <label htmlFor="confirmPassword">Confirm Password</label>
            <input
              type="password"
              name="confirmPassword"
              value={confirmPassword}
              onChange={(e) => setConfirmPassword(e.target.value)}
            />
          </div>
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="city">City</label>
            <input
              type="text"
              name="city"
              value={city}
              onChange={(e) => setCity(e.target.value)}
            />
          </div>
          <div className="form-group">
            <label htmlFor="zipCode">Zip Code</label>
            <input
              type="text"
              name="zipCode"
              value={zipCode}
              onChange={(e) => setZipCode(e.target.value)}
            />
          </div>
        </div>
        <button type="submit">Submit</button>
      </form>
Enter fullscreen mode Exit fullscreen mode

A very regular form inputs, but the problem lies in how the state of those inputs is being controlled;
If you scroll up to the very beginning of our App function you will see those useState hooks;

 const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [phone, setPhone] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [city, setCity] = useState('');
  const [zipCode, setZipCode] = useState('');
Enter fullscreen mode Exit fullscreen mode

And maybe for a small application or a small form that won’t be an issue but for a large piece of code this is not the best way to go, you will have tens of useState hooks in your file and this can get missy fast, it will be harder to read, harder to scale and harder to manage.
Generally speaking to sum the situation up:
if state updates independently - separate useState.
for state that updates together, or only one field at a time updates - a single useState object
for state where user interactions update different parts of the state - useReducer
so for our case, we would go with useReducer

UseReducer to the Rescue
React does offer another - yet somewhat similar to useState - hook, called useReducer

You can use it in your form as follows:

function App() {
  const [formValues, setFormValues] = useReducer(
    reducerFunction,
    initialValues
  );
  ...
}
Enter fullscreen mode Exit fullscreen mode

The useReducer hook like useState gives back two things; the state and the dispatcher and takes two arguments; the reducer function and the initial values of the reducer.
So we will now add the initial values ; an object with the very initial values of our form state, and since both initialValues and reducerFunction are neither changing state nor dependent of any React Hooks, I’m going to create them outside the App component for better performance as follows:

const initialValues = {
  name: '',
  email: '',
  phone: '',
  password: '',
  confirmPassword: '',
  city: '',
  zipCode: '',
};
const reducerFunction = (prevState, newState) => {
  return { ...prevState, ...newState };
};

function App() {
const [formValues, setFormValues] = useReducer(
    reducerFunction,
    initialValues
  );
...
}
Enter fullscreen mode Exit fullscreen mode

Now what happens here, is that our reducerFunction takes two arguments the prevState which is the previous state before the new change or what can be called the current state stored in the useReducer hook and the newState which is the new piece of state sent through setFormValues dispatcher coming from the useReducer hook in the change handler function as follow:

 const onChangeHandler = (event) => {
    const { name, value } = event.target;
    setFormValues({ [name]: value });
  };
Enter fullscreen mode Exit fullscreen mode

This function is going to be passed to ever input as a value to the onChange prop like this:

<input type="email" name="email" value={email} onChange={onChangeHandler} />
Enter fullscreen mode Exit fullscreen mode

Now this function takes the native event as argument and within the function we distructure the event to get the name and value of that input on every change.

We then dispatch the setFormValues function from the useReducer hook which in turn fires the reducerFunction to update the state

So to explain this briefly; for the email input when the value changes the onChangeHandler function fires taking the event as argument
When we distructure the event.target we get name and value which are ‘email’ and the new value of that email input
We then fire setFormValues({ [name]: value }) which translates to setFormValues({ email: emailValue })
Then in the reducer function which fires on that action dispatch
It receives to arguments the prevState and the newState
The prevState is state object of the whole form and the newState is what was passed to setFormValues i.e.

prevState = {
    name,
    ...restOfState,
    email: oldEmailValue
  }
Enter fullscreen mode Exit fullscreen mode

And

newState = {
    email: newEmailValue
  }
Enter fullscreen mode Exit fullscreen mode

In the prevState, name, ...restOfstate here refers to any state you have in the reducer other than the part relevant to us on this step "email"
And thanks to the destructuring ability of ES6, the new email value overrides the old email value and the state is updated

So we are left with one thing, how do we consume the values for the value prop of our inputs and the form submit function?

  const { name, email, phone, password, confirmPassword, city, zipCode, } = formValues;
Enter fullscreen mode Exit fullscreen mode

We will be destructuring the first value of the useReducer hook which is the formValues

So there you have it, how to replace multiple useState with one useReducer 😄🚀

Also please note that if your application does use multiple forms or face complex scenarios with the form validations and the state management of the forms, you can instead of useReducer, use some of the community packages that are more suited for such scenarios e.g. React-hook-form and Formik

Please let me know if you found this article useful and if you have any questions, leave them in the comments and I would be very happy to answer them.

The full code which you can also find on the same repo in final branch, will look like this

import { useReducer } from 'react';
import './App.css';

const initialValues = {
  name: '',
  email: '',
  phone: '',
  password: '',
  confirmPassword: '',
  city: '',
  zipCode: '',
};
const reducerFunction = (prevState, newState) => {
  return { ...prevState, ...newState };
};
function App() {
  const [formValues, setFormValues] = useReducer(
    reducerFunction,
    initialValues
  );

  const onChangeHandler = (event) => {
    const { name, value } = event.target;
    setFormValues({ [name]: value });
  };

  const {
    name,
    email,
    phone,
    password,
    confirmPassword,
    city,
    zipCode,
  } = formValues;
  const submitForm = (e) => {
    e.preventDefault();

    const data = {
      name,
      email,
      phone,
      password,
      confirmPassword,
      city,
      zipCode,
    };
    console.log('data', data);
  };

  return (
    <div className="App" onSubmit={submitForm}>
      <form>
        <div className="form-group">
          <label htmlFor="name">Username</label>
          <input
            type="text"
            name="name"
            value={name}
            onChange={onChangeHandler}
          />
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="email">Email</label>
            <input
              type="email"
              name="email"
              value={email}
              onChange={onChangeHandler}
            />
          </div>
          <div className="form-group">
            <label htmlFor="phone">Phone</label>
            <input
              type="tel"
              name="phone"
              value={phone}
              onChange={onChangeHandler}
            />
          </div>
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="password">Password</label>
            <input
              type="password"
              name="password"
              value={password}
              onChange={onChangeHandler}
            />
          </div>
          <div className="form-group">
            <label htmlFor="confirmPassword">Confirm Password</label>
            <input
              type="password"
              name="confirmPassword"
              value={confirmPassword}
              onChange={onChangeHandler}
            />
          </div>
        </div>
        <div className="two-col_grid">
          <div className="form-group">
            <label htmlFor="city">City</label>
            <input
              type="text"
              name="city"
              value={city}
              onChange={onChangeHandler}
            />
          </div>
          <div className="form-group">
            <label htmlFor="zipCode">Zip Code</label>
            <input
              type="text"
              name="zipCode"
              value={zipCode}
              onChange={onChangeHandler}
            />
          </div>
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)