Your AI coding agent wrote a useEffect that fires forever, and the fix is one line

AI coding agents put objects and functions created during render into useEffect dependency arrays. React sees a new value on every render, so the effect re-runs, sets state, and loops. Here is the mechanism from the React docs and the fix.

Your AI coding agent wrote a component that fetches data. You open the page and the tab freezes, the fan spins up, or your network panel shows the same request firing hundreds of times a minute. Nothing throws an error. The code reads like every useEffect example you have ever seen, and that is exactly the problem.

Why this happens

React re-runs an effect whenever one of its dependencies changed since the last render. To decide that, React compares each dependency with its previous value using Object.is. For strings and numbers that works the way you expect. For objects, arrays, and functions it does not: a new {} or [] created during render is a different object every time, even when the contents are identical.

The React docs say this directly: if your effect depends on an object or a function created during rendering, it might run too often, and using such a function as a dependency will cause the effect to re-run after every commit.

Coding agents produce this pattern constantly, because it is what a “correct” lint-clean effect looks like when the agent follows the exhaustive-deps rule mechanically:

function Results({ query }) {
  const [items, setItems] = useState([])
  const options = { q: query, limit: 20 } // new object every render

  useEffect(() => {
    fetchItems(options).then(setItems)
  }, [options]) // "changed" on every render

  return <List items={items} />
}

Here is the loop: render creates options, the effect runs and calls setItems, the state update triggers a render, that render creates a brand-new options, React sees a changed dependency, and the effect runs again. The docs describe the two conditions for an infinite cycle: your effect updates some state, and that state leads to a re-render that changes the effect’s dependencies. Both are true here.

How to tell if this is your problem

  1. Open the Network tab and look for one endpoint being requested over and over with no user action.
  2. Add a log at the top of the effect (console.log("effect ran")). If it prints continuously, the dependency array is the suspect.
  3. Look at every value in the dependency array that is an object, array, or function declared inside the component body. Those are the ones that change identity on every render.

The fix

Follow the React docs’ advice: do not depend on an object or function created during render. Create it inside the effect instead, and depend on the primitive values it is built from:

useEffect(() => {
  const options = { q: query, limit: 20 }
  fetchItems(options).then(setItems)
}, [query]) // a string: only changes when the query really changes

The same applies to functions: declare the helper inside the effect rather than passing a function from the component body into the dependency array.

If the value genuinely has to live outside the effect, you can stabilize it with useMemo (for objects and arrays) or useCallback (for functions), but reach for moving it inside first. It is simpler, and it leaves nothing for the next edit to break.

Do not “fix” the loop by deleting the dependency array or by silencing the lint rule. That trades a visible loop for an effect that runs with stale values.

How to avoid this next time

When you ask an agent for a data-fetching effect, tell it to keep objects and functions out of the dependency array and to depend on primitives only. When you review its output, scan each useEffect for a dependency array containing something declared with {, [, or => in the component body. That one check catches most render loops before they reach a browser.

Comments