Optimizing Form Handling with useTransition Instead of useState
In React development, managing loading states with boolean values like [isLoading, setIsLoading] is a common pattern, especially for forms and other asynchronous actions. While this approach works, as applications grow in complexity, using useState can negatively impact performance and user experience. Fortunately, React offers tools like useTransition to handle UI transitions more efficiently.
The Typical Approach with useState
Here is a common example of managing a loading state in a form using useState:
import { useState, FormEvent } from "react";
function ContactForm() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
setIsLoading(true);
// Simulate a form submission
await fakeApiRequest();
setIsLoading(false);
alert("Form submitted successfully");
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Your name" required />
<button type="submit" disabled={isLoading}>
{isLoading ? "Sending..." : "Submit"}
</button>
</form>
);
}
function fakeApiRequest(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 2000));
}
export default ContactForm;
In this example, we use useState to disable the submit button while waiting for our "API" response. Although this approach works, it’s not ideal when performance and quick UI responses are critical.
Issues with useState
Performance Issues
When using useState to control a loading state like isLoading, every time we update that state, React re-renders the component. This is fine in simple cases, but frequent updates or more complex logic can lead to performance issues. Changing a boolean like isLoading may cause the UI to momentarily freeze, preventing user interaction until the state changes again.
This happens because React triggers a re-render when it detects a state change with useState. In larger applications with multiple components, this can become unnecessary or even costly, affecting speed and user perception, making the app feel slow or unresponsive.
State Synchronization Issues
In addition to performance, useState can cause synchronization issues, especially when multiple components or hooks depend on the same state. If not managed correctly, there can be situations where the UI shows an inconsistent state, particularly with asynchronous events or rapid state changes.
Managing state updates manually becomes more challenging as the app grows, leading to a more complex codebase with potential synchronization errors. This is where useTransition is valuable for controlling transitions without blocking the UI.
What is useTransition?
useTransition is a hook that allows you to mark a state update as "transitional," meaning React can prioritize more urgent updates (like animations or user interactions) and handle transitions when it’s most convenient.
The key advantage of useTransition is that it improves the perceived performance of the app, enabling React to manage updates more intelligently, offering smoother transitions and better user experiences.
Benefits of Using useTransition
- Improved perceived performance: useTransition allows React to delay non-urgent state updates, enhancing the app’s sense of speed.
- Smoother transitions: Instead of immediately blocking the UI (like when using useState to control isLoading), useTransition allows users to keep interacting with the app while the background operation completes.
- Less code and more declarative: By using useTransition, we avoid writing additional logic to manage the loading state. React automatically marks updates as transitional.
- Flexibility in large applications: If your app has multiple asynchronous operations that could affect performance, useTransition helps control those transitions better, ensuring the UI isn’t impacted by non-urgent operations.
Replacing useState with useTransition
Now, let’s see how our form would look using useTransition to handle the "loading" state. Instead of blocking the UI with setIsLoading(true), we’ll make the form submission a smooth transition that doesn’t interrupt user interactions.
import { useState, useTransition, FormEvent, ChangeEvent } from 'react';
interface FormData {
name: string;
}
function ContactForm() {
const [isPending, startTransition] = useTransition();
const [formData, setFormData] = useState<FormData>({ name: '' });
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
startTransition(async () => {
// Simulate a form submission
await fakeApiRequest();
alert("Form submitted successfully");
});
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Your name"
required
/>
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Submit"}
</button>
</form>
);
}
function fakeApiRequest(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 2000));
}
export default ContactForm;
In this new example, we’ve completely removed the need to manually handle an isLoading state. Instead, we use the isPending boolean provided by useTransition to know if we are in the middle of a transition. The code is cleaner, and we let React manage when and how to update the component’s state more efficiently.
Conclusion
Switching from useState to useTransition to handle states like isLoading may seem like a small adjustment, but in larger applications, this approach can make a significant difference in terms of performance and user experience. Transition handling becomes much smoother, and React intelligently optimizes UI updates.