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
- Open the Network tab and look for one endpoint being requested over and over with no user action.
- Add a log at the top of the effect (
console.log("effect ran")). If it prints continuously, the dependency array is the suspect. - 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
Sign in to join the conversation.
No comments yet — be the first.