You Don't Need useState

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

User Avatar

Faisal Husain

You Don't Need useState blog image

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

Want to hire me as a freelancer? Let's discuss.
Drop a message and let's discuss
HomeAboutProjectsBlogsHire MeCrafts