DEV Community

Cover image for The Infinite Re-render That Froze My App
Prince Patel
Prince Patel

Posted on Edited on

The Infinite Re-render That Froze My App

Summer Bug Smash: Smash Stories 🐛🛹

React’s “Maximum Update Depth Exceeded”: The useEffect Bug That Caused Hundreds of API Calls

One useEffect dependency caused my React application to make the same API request hundreds of times.

The browser became sluggish, CPU usage went up, and eventually React crashed with:

Maximum update depth exceeded.
Enter fullscreen mode Exit fullscreen mode

At first, I thought the problem was somewhere in my API or state management.

It wasn't.

The problem was hiding inside a small useEffect.

What Happened?

Everything looked normal at first.

The application compiled successfully.
There were no syntax errors.
The UI loaded correctly.

Then I clicked a button.

A few seconds later, the application started slowing down.

I opened Chrome DevTools and checked the Network tab.

That's when I noticed something strange:

The same API request was being sent repeatedly.

Not twice.

Not ten times.

Hundreds of times.

Every response seemed to trigger another render, which triggered another request.

So I started tracing the component to find out what was causing the loop.

The Culprit: useEffect

I eventually found code similar to this:

const [users, setUsers] = useState([]);

useEffect(() => {
  fetchUsers().then((data) => {
    setUsers(data);
  });
}, [users]);
Enter fullscreen mode Exit fullscreen mode

At first glance, the effect looks reasonable.

But look carefully at the dependency:

[users]
Enter fullscreen mode Exit fullscreen mode

The effect depends on users.

But inside the effect, we're also updating users:

setUsers(data);
Enter fullscreen mode Exit fullscreen mode

That creates a cycle.

Why Does It Loop?

Here's what happens:

Component renders
       ↓
useEffect runs
       ↓
API request starts
       ↓
Data comes back
       ↓
setUsers(data)
       ↓
users changes
       ↓
Component renders again
       ↓
useEffect runs again
       ↓
Another API request
       ↓
... repeat ...
Enter fullscreen mode Exit fullscreen mode

Eventually, React throws:

Maximum update depth exceeded
Enter fullscreen mode Exit fullscreen mode

The important thing to remember is:

If an effect updates a state value that the same effect depends on, you may have created a render loop.

The Fix

In this case, the effect only needed to fetch the users when the component mounted.

So the dependency array should not listen to users:

const [users, setUsers] = useState([]);

useEffect(() => {
  fetchUsers().then((data) => {
    setUsers(data);
  });
}, []);
Enter fullscreen mode Exit fullscreen mode

Now the flow becomes:

Component renders
       ↓
useEffect runs once
       ↓
API request
       ↓
setUsers(data)
       ↓
Component renders
       ↓
useEffect does NOT run again
Enter fullscreen mode Exit fullscreen mode

Result:

One request instead of hundreds.

The Dependency Array Mental Model

Whenever I write a new useEffect, I now think about it like this:

Dependency changes
       ↓
Effect runs
       ↓
Does the effect update that dependency?
       ↓
       YES
       ↓
Could the effect trigger itself again?
       ↓
       ⚠️
Enter fullscreen mode Exit fullscreen mode

That simple check can prevent a lot of debugging pain.

The dependency array isn't just a list of values you happen to use.

It determines when React should run the effect again.

Common useEffect Mistake

A common pattern is:

useEffect(() => {
  setSomething(value);
}, [something]);
Enter fullscreen mode Exit fullscreen mode

Whenever you see an effect structured like this, stop and ask:

"What happens after this state changes?"

If changing the state causes the effect to run again, you need to make sure the cycle has a valid stopping condition.

Before vs After

Before

  • Browser became unresponsive
  • Hundreds of API requests
  • High CPU usage
  • Repeated renders
  • Maximum update depth exceeded
  • Poor user experience

After

  • One API request
  • Stable rendering
  • Smooth navigation
  • Lower unnecessary CPU usage
  • Cleaner component logic

What I Learned

This bug reinforced a few React fundamentals for me:

1. Every state update can trigger another render.

Calling:

setUsers(data);
Enter fullscreen mode Exit fullscreen mode

doesn't just change a variable. It causes React to render the component again.

2. Dependencies control when an effect runs.

A dependency changing can cause the effect to execute again.

3. Be careful when an effect changes one of its own dependencies.

That combination can easily create an infinite loop.

4. Don't add dependencies blindly.

The goal isn't to make the dependency array longer.

The goal is to correctly describe when the effect needs to run.

Could Monitoring Have Helped?

Performance and error-monitoring tools can make problems like this easier to investigate.

For example, monitoring can help identify:

  • Repeated errors
  • Excessive network activity
  • Performance degradation
  • Session behaviour before the crash

That can significantly reduce the time needed to find the root cause.

Final Thought

This wasn't caused by a complicated algorithm or an obscure library.

It came from a tiny dependency mistake:

[users]
Enter fullscreen mode Exit fullscreen mode

That's what makes React bugs like this interesting.

A small change can create a surprisingly large performance problem.

Now, whenever I write a new useEffect, I ask myself one question:

"Could this effect accidentally trigger itself?"

That question has already saved me from repeating this mistake.

Have you ever encountered an infinite useEffect loop? What caused yours?

Top comments (0)