Back to blogs

How I Fixed the Most Annoying useState and useEffect Bugs in React

August 24, 2025
7 min read
How I Fixed the Most Annoying useState and useEffect Bugs in React

Over the past couple of years working with React, I’ve made almost every mistake you can imagine with useState and useEffect. At first, these two hooks felt simple. But once I started building real apps, I realized how easy it is to fall into traps that lead to bugs, performance issues, and hours of frustration.


What I’m sharing here are mistakes I’ve personally made, how I discovered them (usually the hard way), and the patterns I now follow in 2025. Hopefully, my lessons save you some of the debugging pain I went through.


1. Initializing useState Incorrectly

Early on, I would often just start useState with undefined or an empty value, thinking I’d update it later. But that caused crashes when I tried to access properties on something that didn’t exist.


For example, I once initialized a user state like this:

const [user, setUser] = useState();
// later in render:
<p>{user.name}</p> // 💥 Cannot read property 'name' of undefined


This bug wasted me hours because I thought the issue was with my API call, but it was really just bad initialization.

Now I always match the initial shape of the data. If I expect an object, I start with an object.


const [user, setUser] = useState({ name: '', image: '', bio: '' });


This way, the component doesn’t break while waiting for real data to load. It also makes my code cleaner because I don’t have to check for undefined everywhere.


2. Directly Mutating State

I can’t count how many times I broke my component because I tried to directly mutate state. At first, it felt natural to do something like this:


// ❌ Wrong
setUser((user) => (user.name = "Mark"));


I assumed I was just updating the name, but what I really did was turn the state into a string, because the assignment expression returns "Mark". React didn’t even re-render correctly.

What I learned is that React depends on immutability. You always need to create a new reference so React knows something changed.


// ✅ Correct
setUser((prev) => ({ ...prev, name: "Mark" }));


This pattern solved so many weird bugs where the UI wasn’t updating even though I thought I had “changed” the state.


3. Forgetting That useState Doesn’t Merge Objects

When I first moved from class components to hooks, I assumed useState would merge objects like this.setState did. I quickly found out that wasn’t true.

I wrote code like this:


setUser({ age: 31 }); // Oops! name is gone


Suddenly, my user’s name would vanish whenever I updated their age. That’s when I realized React hooks replace state instead of merging it.


The fix was the same spread operator pattern:


setUser((prev) => ({ ...prev, age: 31 }));


This way, the old properties stick around, and only the updated fields change. It’s a small difference, but forgetting it cost me lots of debugging time.


4. Assuming setState Is Synchronous

This mistake hit me especially when I was working with counters. I assumed calling setCount(count + 1) multiple times would keep incrementing properly.


setCount(count + 1);
setCount(count + 1); // Expected 2 higher, but only 1 higher


React batches state updates, so it often uses the “old” value when you do this. My counter kept behaving strangely until I learned about functional updates.


setCount((prev) => prev + 1);


Once I switched to this pattern, I never had those inconsistent results again. Now I use functional updates whenever the next state depends on the previous one.


5. Forgetting the Dependency Array in useEffect

One of my worst habits early on was writing effects without a dependency array. That caused my API calls to run on every render, which quickly turned into infinite loops.


useEffect(() => {
fetchData(); // Ran way too often
});


The solution was simple: always add a dependency array, and make sure it’s correct.


useEffect(() => {
fetchData();
}, [someVariable]);


If nothing needs to change, I use an empty array so it runs only once on mount. Now I treat the dependency array as a mandatory checklist item.


6. Not Cleaning Up Effects

This one took me longer to realize because the bugs didn’t show up immediately. I once added an interval in useEffect but forgot to clear it. Over time, I noticed multiple intervals stacking up, slowing the app down.


useEffect(() => {
const timer = setInterval(() => console.log("tick"), 1000);
}, []);


The right way is to return a cleanup function:


useEffect(() => {
const timer = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(timer);
}, []);


Now I make it a rule: if I add something in an effect, I also clean it up there.


7. Using useEffect for Derived State

Another trap I fell into was overusing useEffect. I used it for calculations that didn’t need it, like doubling a counter.


const [count, setCount] = useState(0);
const [double, setDouble] = useState(0);

useEffect(() => {
setDouble(count * 2);
}, [count]);


This works, but it’s unnecessary. Now I just compute values directly:


const double = count * 2;


It makes the code simpler and avoids an extra render cycle. I save effects only for actual side effects, not basic calculations.


8. Stale State in Async Events

This one was sneaky. I wrote a delayed counter increment using setTimeout, but it didn’t always increment correctly.


function handleClick() {
setTimeout(() => {
setCount(count + 1); // sometimes stale
}, 1000);
}


The closure captured an old value of count, so by the time the timeout fired, it was outdated. The fix was (again) the functional update:


function handleClick() {
setTimeout(() => {
setCount((prev) => prev + 1);
}, 1000);
}


That small change made my async code reliable.


9. Storing Global State with useState

In my early projects, I put everything into useState—even global stuff like authentication or theme. That quickly became messy as I passed state down through multiple levels of props.

Now I only use useState for local, component-specific data. For anything global, I use Context or a state management library like Zustand or Redux. It keeps the app cleaner and prevents prop-drilling nightmares.


10. Case Study: Fixing a “Broken” Counter

One of my most memorable bugs was a delayed counter that always seemed “one step behind.”


const [counter, setCounter] = useState(0);
const handleDelayedIncrement = () => {
setTimeout(() => setCounter(counter + 1), 1000);
};


No matter how many times I clicked, the counter lagged. The issue was stale state again.


The fix was switching to a functional update:

const handleDelayedIncrement = () => {
setTimeout(() => setCounter((prev) => prev + 1), 1000);
};


After that, it worked exactly as expected.


Best Practices I Follow Now

Looking back, most of my mistakes came from misunderstanding how React handles state and effects. Here are the habits I stick to now:


  1. Always initialize state with the right type or structure.
  2. Never mutate state directly; always create a new object or array.
  3. Treat the dependency array in useEffect as mandatory.
  4. Always clean up side effects like intervals or event listeners.
  5. Use functional updates whenever the next state depends on the previous one.
  6. Keep state as local as possible, and use Context or a library for global needs.
  7. Don’t overuse useEffect—only use it for real side effects, not derived values.


These lessons cost me plenty of late nights, but they’ve made me a much stronger React developer. If you’re just starting out, I hope my mistakes help you avoid the same struggles. Mastering useState and useEffect is still the key to writing solid React apps in 2025.

reactuseStateuseEffectreact hooksreact mistakesreact debuggingreact beginnersreact errorsreact state managementreact functional componentsreact tipsreact best practicesreact 2025react guidefix useStateuseEffect not workingusestate not updating immediatelyreact usestate common mistakesreact useeffect dependency array explainedwhy is my usestate not workingreact hooks best practices 2025avoid common mistakes in react hooksstale state in react useeffecthow to fix memory leaks in reactuseeffect cleanup function examplereact beginner mistakes with hooksuseeffect vs usestate when to usereact usestate not merging objecthow to debug react hooksreal world examples of react useeffect

Recent Blogs

View All