React infinite loop: setState in useEffect dependency
This error hits when setState inside useEffect triggers a re-render that runs the same effect again. The fix is removing the state variable from the dependency array.
You'll see this error when a component re-renders more than 50 times in a row — React's safety limit. The classic scenario: you fetch data in a useEffect, call setState(data), and the state variable is also in the effect's dependency array. Each setState triggers a re-render, which re-runs the effect, which calls setState again. Boom. Infinite loop.
I've seen this on React 16.8 through 18.2. Happens most often with custom hooks or when somebody adds a state variable to dependencies thinking it will "keep the effect in sync." It won't. It'll choke your browser tab.
Root cause: the dependency trap
The dependency array tells React: "run this effect when any of these values change." When you put a state variable in there and also update that same state inside the effect, you create a feedback loop. The state changes -> effect re-runs -> state changes again -> repeat until React gives up.
Newer React versions (18+) with strict mode make this worse because effects run twice in development. That double-run can expose loops that were hidden in production. Don't ignore it — it'll still crash in production under the right conditions.
The fix in 4 steps
- Identify the state variable causing the loop. Look at your dependency array. Find the state variable that's being set inside the same effect. That's your culprit.
- Remove the state variable from the dependency array. If the effect only needs to run once (or on mount/unmount), use an empty array
[]. If it depends on a prop or another state, list only those — not the state being set. - Use a ref if you must track the previous value. Sometimes you need the latest state inside the effect without triggering re-runs. That's what
useRefis for. Example:const latestState = useRef(state); useEffect(() => { latestState.current = state; }); // no deps — runs after every render but doesn't cause re-render - Use the functional updater form of setState. If you need to update state based on its previous value, do this:
This way you don't need the state value in the dependency array at all. The effect can run withsetState(prev => prev + 1);[]or whatever minimal deps you need.
Another common pattern: data fetching
You fetch data on mount, but you also want to refetch when a filter changes. The mistake is putting the fetched data in the dependency array:
// Broken
const [data, setData] = useState([]);
const [filter, setFilter] = useState('all');
useEffect(() => {
fetch('/api/data?filter=' + filter)
.then(res => res.json())
.then(setData);
}, [data, filter]); // BUG: data causes loopRemove data from the array. Only keep filter. If the effect does a second fetch after the first one sets data, that's a different bug — usually means your backend is returning different results each time.
What to check if it still fails
- Check for multiple effects. Two separate effects can create a cross-loop: effect A sets state X, effect B depends on X and sets state Y, effect A depends on Y and sets X again. You'll see this in complex forms or state machines. Break them into one effect or use
useReducer. - Look at parent components. If the parent is passing a new object/array on every render (like
<Child data={{x:1}} />), the child'suseEffectwill see a new reference each time. Memoize withuseMemoorReact.memo. - Disable React strict mode temporarily. If the loop only happens in development, you might have a side-effect that's safe to run twice but not 50 times. Still fix it — don't just disable strict mode. Use
useEffectcleanup to cancel the previous fetch:useEffect(() => { let cancelled = false; fetch(url).then(res => { if (!cancelled) setState(res); }); return () => { cancelled = true; }; }, [url]); - Check for
useCallbackissues. If a function in the dependency array is recreated on every render, it'll trigger the effect. Wrap it inuseCallbackwith proper deps. - Console.log each render. Add a
console.log('render', state)at the top of your component. See how many times it logs before the error. That'll tell you exactly which state change is causing the chain reaction.
Most of the time, removing the state from the dependency array solves it. If it doesn't, you're dealing with a cycle that involves two or more effects — or a parent re-creating objects. Either way, the principle is the same: never put a state variable in a dependency array if that same effect sets that state. Period.
Was this solution helpful?