You Don't Need useState
Learn when not to use the "useState" hook and how useRef can help you build more performant React apps.

Faisal Husain

Have you ever built a React page that felt snappy—until you added more content? I ran into this exact issue at work. My app started with 20 cards and a textarea, but when I scaled up to 1000 cards, typing in the textarea became painfully slow. The culprit? Overusing useState for values that don't need to trigger a re-render.
In this post, I'll show you:
- Why excessive use of
useState
can hurt performance - How switching to
useRef
can make your UI smooth again
The Problem: Laggy Inputs with useState
Let's start with a simple scenario. I built a page with 20 cards and a textarea at the bottom. Typing in the textarea was smooth. Watch the demo below:
Here's the code for this component:
// 20 cards, input is managed with useState
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { faker } from "@faker-js/faker";
import { useState } from "react";
const CardsWithStateInput = () => {
const generateCards = () => {
return Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
content: ` ${faker.hacker.phrase()}`
}));
};
const [cards, setCards] = useState(generateCards());
const [inputText, setInputText] = useState('');
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputText.trim()) {
const newCard = {
id: cards.length + 1,
content: inputText
};
setCards([...cards, newCard]);
setInputText('');
}
};
return (
<div className="w-full p-4 bg-gray-100 min-h-screen">
<h2 className="text-xl font-bold mb-4">Cards Component (using useState)</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{cards.map(card => (
<Card key={card.id}>
<CardHeader>
<CardTitle>Card #{card.id}</CardTitle>
</CardHeader>
<CardContent>
<p>{card.content}</p>
</CardContent>
</Card>
))}
</div>
<form className="sticky bottom-4 bg-white p-4 rounded-lg shadow-md" onSubmit={handleSubmit}>
<textarea
value={inputText}
onChange={handleInputChange}
placeholder="Add a new card..."
className="w-full p-2 border border-gray-300 rounded-md mb-2"
rows={3}
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
>
Add Card
</button>
</form>
</div>
);
};
export default CardsWithStateInput;
With just 20 cards, everything feels fine. But what happens if we scale up to 1000 cards?
Scaling Up: The Lag Becomes Unbearable
Watch what happens when I increase the card count to 1000 and try to type quickly in the textarea:
Here's the code for this component:
// 1000 cards, still using useState for input
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { faker } from "@faker-js/faker";
import { useState } from "react";
const CardsWithStateInput = () => {
const generateCards = () => {
return Array.from({ length: 1000 }, (_, index) => ({
id: index + 1,
content: ` ${faker.hacker.phrase()}`
}));
};
const [cards, setCards] = useState(generateCards());
const [inputText, setInputText] = useState('');
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputText.trim()) {
const newCard = {
id: cards.length + 1,
content: inputText
};
setCards([...cards, newCard]);
setInputText('');
}
};
return (
<div className="w-full p-4 bg-gray-100 min-h-screen">
<h2 className="text-xl font-bold mb-4">Cards Component (using useState)</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{cards.map(card => (
<Card key={card.id}>
<CardHeader>
<CardTitle>Card #{card.id}</CardTitle>
</CardHeader>
<CardContent>
<p>{card.content}</p>
</CardContent>
</Card>
))}
</div>
<form className="sticky bottom-4 bg-white p-4 rounded-lg shadow-md" onSubmit={handleSubmit}>
<textarea
value={inputText}
onChange={handleInputChange}
placeholder="Add a new card..."
className="w-full p-2 border border-gray-300 rounded-md mb-2"
rows={3}
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
>
Add Card
</button>
</form>
</div>
);
};
export default CardsWithStateInput;
Typing random words very fast? The textarea now feels completely stuck. Why?
Technical Explanation
Every time you type in the textarea, setInputText triggers a re-render of the entire component—including all 1000 cards. React has to re-render every card on every keystroke! This is why the UI becomes unresponsive as the list grows.
The Solution: useRef for Transient Input
To fix this, we can use useRef for the textarea value. This way, typing in the textarea won't trigger a re-render of the cards—only the actual addition of a new card will update the UI.
Watch the improved result below:
Here's the code using useRef:
// 1000 cards, input managed with useRef
"use client"
import { useRef, useState } from "react";
import { faker } from '@faker-js/faker';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const CardsWithRefInput = () => {
const generateCards = () => {
return Array.from({ length: 1000 }, (_, index) => ({
id: index + 1,
content: ` ${faker.hacker.phrase()}`
}));
};
const [cards, setCards] = useState(generateCards());
const textInputRef = useRef<HTMLTextAreaElement | null>(null);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (!textInputRef.current) {
return;
}
const inputText = textInputRef.current.value;
if (!inputText.trim()) return;
const newCard = {
id: cards.length + 1,
content: inputText
};
setCards([...cards, newCard]);
textInputRef.current.value = "";
};
return (
<div className="w-full p-4 bg-gray-100 min-h-screen">
<h2 className="text-xl font-bold mb-4">Cards Component (using useRef)</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{cards.map(card => (
<Card key={card.id}>
<CardHeader>
<CardTitle>Card #{card.id}</CardTitle>
</CardHeader>
<CardContent>
<p>{card.content}</p>
</CardContent>
</Card>
))}
</div>
<div className="sticky bottom-4 bg-white p-4 rounded-lg shadow-md">
<form onSubmit={handleSubmit}>
<textarea
ref={textInputRef}
placeholder="Add a new card..."
className="w-full p-2 border border-gray-300 rounded-md mb-2"
rows={3}
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
>
Add Card
</button>
</form>
</div>
</div>
);
};
export default CardsWithRefInput;
Now, you can type as fast as you want even with 1000 cards! The textarea remains responsive, because updating the ref does not trigger a re-render of the card list.
Conclusion
The key takeaway: Not every value needs to be in state. For values that don’t affect rendering like the current value of an input useRef is often a better choice. This simple change can make a huge difference in your app’s performance.
Example Repo: I built this simple app to show you the issue