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:
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.
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:
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.
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:
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:
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.
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.
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.
The solution was simple: always add a dependency array, and make sure it’s correct.
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.
The right way is to return a cleanup function:
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.
This works, but it’s unnecessary. Now I just compute values directly:
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.
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:
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.”
No matter how many times I clicked, the counter lagged. The issue was stale state again.
The fix was switching to a functional update:
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:
- Always initialize state with the right type or structure.
- Never mutate state directly; always create a new object or array.
- Treat the dependency array in
useEffect
as mandatory. - Always clean up side effects like intervals or event listeners.
- Use functional updates whenever the next state depends on the previous one.
- Keep state as local as possible, and use Context or a library for global needs.
- 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.