Photo by Lautaro Andreani on Unsplash
Don’t use State for React Forms. Use this instead!
Learn how to optimize the performance of react forms by replacing useState with the native feature of JavaScript.
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
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
The form input values are automatically captured without the need to maintain a state variable for each input field.
The component doesn't re-render on user input.
The API request body can be easily built when using
FormData
, whereas we would need to assemble the data for submission when usinguseState
.It eliminates the need for introducing new state variables as and when the form grows.
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.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 withuseState
requires additional handling, whereasFormData
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!