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

Nirmal Kumar
JavaScript in Plain English
5 min readAug 31, 2023

--

Learn how to optimize the performance of react forms by replacing useState with the native feature of JavaScript.

Photo by Lautaro Andreani on Unsplash

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 tend to use hooks that comes with it. Using hooks solve a lot of problems in React, but is it really 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 everytime the input (state) changes.

Side Tip: I came across this answer on StackOverflow which is very useful to count the number of times a component has 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 cleanup unwanted files once the project is created.

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

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

# pnpm
pnpm create vite my-react-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 useState to manage the form inputs.

import { useEffect, useRef, useState } from "react";

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

User entering the email and password. The component render count is updated as and when the user types in.
Form state update with render count

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 also 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 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 theuseState 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 create 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.

User entering the email and password. The component render count is updated as and when the user types in.
Form Without State GIF

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 that, 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 for Reading.

In Plain English

Thank you for being a part of our community! Before you go:

--

--