TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

How to Fix React useEffect Infinite Loops

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20268 min read5 topics
How to Fix React useEffect Infinite Loops
Explore the details
#React#useEffect#React Hooks#debugging#JavaScript
October 26, 20268 min read

A React useEffect that repeatedly updates state can trigger an infinite render loop. The fix is not usually to hide a dependency; it is to identify what the effect synchronizes, which dependency changes after the state update, and whether the work belongs in an effect at all.

Key Takeaways

  • An effect can loop when it updates state and that update changes one of the effect’s dependencies.
  • Objects and functions created during rendering get new identities on each render, even when their contents look the same.
  • Include the values an effect reads. Prefer restructuring the effect over removing dependencies to silence a warning.
  • If a value can be calculated from props or state during rendering, calculate it there instead of storing a duplicate in state.
  • Clean up subscriptions, timers, and other external resources in the effect’s cleanup function.

Why useEffect can loop

React runs an effect after a render. When its dependencies change, React runs the previous cleanup, if there is one, and then runs the effect again. If the effect updates state, React renders again. A loop occurs when that render causes the effect to run again, and the cycle continues.

For example, this effect depends on count and also changes count:

import { useEffect, useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  }, [count]);

  return <p>{count}</p>;
}

Each run increments count, so the dependency changes and triggers another run. The effect is doing work that should not happen automatically after every change. If the increment should happen when a person clicks a button, put it in the event handler:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(current => current + 1);
  }

  return <button onClick={handleIncrement}>Count: {count}</button>;
}

Here, the state update is tied to the user action; no effect is needed.

Check the dependency array

The dependency array determines when React re-runs an effect:

  • With no dependency array, the effect runs after every commit.
  • With an array such as [roomId], it runs after the initial commit and when roomId changes.
  • With [], it runs after the initial commit, with an additional development-only setup and cleanup cycle under Strict Mode.

An effect without dependencies can loop if it updates state on every run:

useEffect(() => {
  setValue(value + 1);
});

If an effect really needs to synchronize something with the current value, include the dependencies it reads and make sure its state update does not cause the same synchronization to repeat without a reason. Do not remove dependencies just to stop the loop: that can leave the effect using stale values or fail to resynchronize when inputs change.

A dependency array is not a scheduling switch for arbitrary code. First ask whether the work is synchronization with an external system, such as a connection, subscription, or browser API. If it is only a calculation from current props and state, do that calculation during rendering instead.

Fix object and function identity problems

React compares dependencies by identity. A new object or function created during rendering is different from the one created on the previous render, even if its contents or code are identical.

Objects created during render

This example creates a new options object on every render. If the effect updates state, that update triggers a render, the object gets a new identity, and the effect runs again:

function ChatRoom({ roomId }) {
  const options = { serverUrl: 'https://chat.example', roomId };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    setStatus('connected');

    return () => connection.disconnect();
  }, [options]);
}

If the effect only needs the object’s primitive values, create the object inside the effect and depend on those values instead:

function ChatRoom({ roomId }) {
  const [status, setStatus] = useState('disconnected');

  useEffect(() => {
    const options = { serverUrl: 'https://chat.example', roomId };
    const connection = createConnection(options);
    connection.connect();
    setStatus('connected');

    return () => connection.disconnect();
  }, [roomId]);

  return <p>{status}</p>;
}

This makes the effect respond to a change in roomId, rather than a fresh object created during each render. In a real application, use the connection API and state updates that match your implementation.

Functions created during render

A function declared inside a component also gets a new identity on each render. If an effect depends on that function, it may run after every render:

function Results({ query }) {
  const getResults = () => search(query);

  useEffect(() => {
    setResults(getResults());
  }, [getResults]);
}

When the function is only needed by the effect, define it inside the effect and depend on the values it uses:

function Results({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const nextResults = search(query);
    setResults(nextResults);
  }, [query]);

  return <ResultsList results={results} />;
}

If a function must be shared outside the effect, consider whether it can be declared outside the component, or use useCallback when stable function identity is actually needed. Memoization is not a substitute for understanding the effect’s dependencies.

Avoid storing values you can derive

A common loop starts with an effect that copies a derived value into state, then lists that state as a dependency. If the derived value is only needed for rendering, calculate it directly instead.

// Avoid: filteredItems is derived from items and query.
useEffect(() => {
  setFilteredItems(items.filter(item => item.name.includes(query)));
}, [items, query, filteredItems]);

Use a render-time calculation:

function ItemList({ items, query }) {
  const filteredItems = items.filter(item => item.name.includes(query));

  return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

If the calculation is measurably expensive, memoization may be appropriate. For ordinary calculations, start with the simpler direct version.

Use functional state updates when appropriate

Sometimes an effect needs to update state based on its previous value. A functional updater lets React provide the previous value, so the effect does not need to read that state variable from its closure.

For example, a timer can increment a counter without restarting on every increment:

import { useEffect, useState } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(current => current + 1);
    }, 1000);

    return () => clearInterval(timerId);
  }, []);

  return <p>Elapsed ticks: {count}</p>;
}

The empty dependency array is appropriate here because the interval setup does not read reactive values from the component. The functional updater receives the latest state. The cleanup clears the interval when the effect is cleaned up, preventing an obsolete timer from continuing to run.

Do not use a functional updater simply to hide a dependency when the effect needs the current value for other work. The effect still needs to declare the reactive values it reads.

Understand cleanup and Strict Mode

An effect cleanup runs before React re-runs that effect with changed dependencies and when the component is removed. Use it to undo or release what the effect set up: unsubscribe from a listener, disconnect a connection, or clear a timer.

In development, Strict Mode runs an extra setup-and-cleanup cycle to help reveal effects that are not safe to repeat. This can make a request, log, or connection appear to happen twice during development. Check that setup and cleanup are correctly paired; do not remove dependencies or disable Strict Mode solely to conceal the behavior. This development check is not itself proof of an infinite loop.

A practical debugging workflow

  1. Find the effect that repeats. Add a temporary log inside its setup and, if useful, its cleanup. Check whether it runs after every render or only when particular values change.
  2. List what the effect reads. Include props, state, and values declared in the component that the effect uses.
  3. Inspect what it updates. If it calls a state setter, determine whether that update changes one of the dependencies or creates a new object or function used by the effect.
  4. Check whether an effect is needed. Move user-triggered work to an event handler. Calculate derived display values during rendering. Keep effects for synchronization with external systems.
  5. Restructure, then verify dependencies. Move effect-only objects or functions inside the effect, use a functional updater when appropriate, and keep the dependency list accurate. Pay attention to warnings from the React Hooks lint rules.
  6. Check cleanup. Ensure subscriptions, timers, or connections are released before the effect is restarted or the component is removed.

Common Mistakes

  • Deleting a dependency to stop repeated runs. This can cause stale values and missed updates; fix the effect’s structure instead.
  • Adding [] without checking what the effect reads. An empty array does not mean “run this whenever the values I care about change.”
  • Putting event logic in an effect. A click-driven update usually belongs in the click handler, not in an effect that watches state.
  • Assuming objects are compared by their contents. A newly created object has a new identity, even when its properties are unchanged.
  • Treating Strict Mode’s extra development cycle as an infinite loop. Check whether setup and cleanup repeat as a finite pair or whether renders continue indefinitely.
  • Keeping redundant state. If a value can be derived from existing props or state, storing and synchronizing a second copy can create unnecessary render cycles.

Best Practices

  • Keep each effect focused on one external synchronization task.
  • Include all reactive values used by the effect, and address lint warnings by restructuring rather than suppressing them without a clear reason.
  • Prefer primitive dependencies where possible; create effect-specific objects and functions inside the effect.
  • Use functional state updates when an update depends only on the previous state.
  • Make setup and cleanup a matched pair for timers, subscriptions, and connections.
  • Use React DevTools and temporary logs to identify which values change between renders; remove diagnostic logging after troubleshooting.

Conclusion

To fix a React useEffect infinite loop, trace the state update and the dependency that changes afterward. Then decide whether the work belongs in an effect, make object and function dependencies stable by restructuring where they are created, and keep cleanup and dependency declarations accurate. Removing dependencies is not a safe general-purpose fix.

References