Don’t use State for React Forms. Use this instead!

Nirmal KumarNirmal Kumar
5 min read

Introduction

When it comes to handling forms in react, the most popular approach is to store the input values in state variables. One of the reasons for following this approach is because, it's React, after all, and everyone tends to use the hooks that come with it. Using hooks solves a lot of problems in React, but is it required when it comes to forms? Let's check it out.

Problem with using States

As we already know, whenever the value of the state variable changes inside a component, react will re-render the component to match its current state. Though it's not a big issue in small applications, it may cause performance bottlenecks as your application grows in size. When it comes to form, React will attempt to re-render the component every time the input (state) changes.


Side Tip: I came across this answer on StackOverflow which is very useful for counting the number of times a component has been rendered. We will use that utility function in our code as well.


Let's implement and see the issue with states in action.

Create a basic react app using Vite and clean up unwanted files once the project is created.

npm create vite@latest my-vue-app -- --template react

# yarn
yarn create vite my-vue-app --template react

# pnpm
pnpm create vite my-vue-app --template react

Let's create a react component (say FormWithState) containing a form that takes in two inputs email and password. We will use the state to manage the form inputs.

import { useEffect, useRef, useState } from "react";
import "./Forms.css";

export default function FormWithState() {
  // The count will increment by 2 on initial render due to strict mode then by 1 on subsequent renders
  const countRef = useRef(0);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  useEffect(() => {
    countRef.current = countRef.current + 1;
  });

  function handleSubmit(e) {
    e.preventDefault();
    console.log({ email, password });
  }

  return (
    <div className="form-div">
      <h2>Form With State</h2>
      <form onSubmit={handleSubmit}>
        <div className="input-field">
          <label htmlFor="email2">Email</label>
          <input
            id="email2"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            autoComplete="off"
          />
        </div>
        <div className="input-field">
          <label htmlFor="password2">Password</label>
          <input
            id="password2"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Submit</button>
        <div>
          <p>
            The Component Re-Rendered <span>{countRef.current}</span> times
          </p>
        </div>
      </form>
    </div>
  );
}

Add this component to the App component and open http://localhost:5173

Form With State GIF

As you can see, the form component is rendered about 23 times and the count will increase gradually as the number of input fields increases. In most cases, the form values are used only during the form submission. So, is it required to re-render the component about 20+ times just for two input fields? The answer is a clear NO!

Also, when the number of input fields increases, the number of state variables to store the input values increases, thereby increasing the complexity of the codebase. So, what's the alternative approach to avoid re-renders but achieving all the functionalities of the forms?

Using FormData to handle forms

So, the alternative approach is to use the native FormData interface of JavaScript.

There are three ways to create a new FormData object as described in the official docs.

new FormData();
new FormData(form);
new FormData(form, submitter);

We will be using the second method because we already have a form. We just need to pass the form element to the constructor and it will auto-populate the form values. To make this work, we also need to add the name attribute to the input tag. Let's test this approach. Create a component (say FormWithoutState).

import { useEffect, useRef } from "react";

export default function FormWithoutState() {
  // The count will increment by 2 on initial render due to strict mode then by 1 on subsequent renders
  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = countRef.current + 1;
  });

  function handleSubmit(e) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    const email = form.get("email");
    const password = form.get("password");
    console.log({ email, password });
    const body = {};
    for (const [key, value] of form.entries()) {
      body[key] = value;
    }
    console.log(body);
    // Do Further input validation and submit the form
  }

  return (
    <div className="form-div">
      <h2>Form Without State</h2>
      <form onSubmit={handleSubmit}>
        <div className="input-field">
          <label htmlFor="email1">Email</label>
          <input id="email1" type="email" name="email" autoComplete="off" />
        </div>
        <div className="input-field">
          <label htmlFor="password1">Password</label>
          <input id="password1" type="password" name="password" />
        </div>
        <button type="submit">Submit</button>
        <div>
          <p>
            The Component Re-Rendered <span>{countRef.current}</span> times
          </p>
        </div>
      </form>
    </div>
  );
}

In this component, we haven't used useState hook at all. Instead, we are adding the name attribute to the input tag. Once the user submits the form, in the handleSubmit function, we are creating the FormData by providing the form object via e.currentTarget. Then we iterate through the FormData.entries() method to get the form key and value to build the form body. We can then use this object for further input validation and submission via fetch or Axios API. But, what about the impact of component re-rendering of this approach? Let's check it out. Add this component to the App component and open http://localhost:5173.

Aren't you surprised? The component didn't re-render at all.

Advantages of using FormData

  1. The form input values are automatically captured without the need to maintain a state variable for each input field.

  2. The component doesn't re-render on user input.

  3. The API request body can be easily built when using FormData, whereas we would need to assemble the data for submission when using useState.

  4. It eliminates the need for introducing new state variables as and when the form grows.

  5. When dealing with multiple forms, you might end up duplicating similar state variables across components, whereas FormData can be reused easily with just a few lines of code.

  6. One thing that FormData supports out of the box is, it will handle dynamic fields automatically. i.e., If your form has dynamically generated fields (adding/removing fields based on user input), managing their state with useState requires additional handling, whereas FormData will take care of it automatically.

Conclusion

You can check the code for this article on code sandbox here. Hope you learned something new from this article. Leave a comment if you have any doubts. Thanks!

1
Subscribe to my newsletter

Read articles from Nirmal Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nirmal Kumar
Nirmal Kumar

Full stack software Engineer https://linktr.ee/NirmalKumar