React Native Screen Flickering or Re-Rendering Infinitely in useEffect
An infinite re-render loop in a React Native screen almost always traces back to a useEffect whose dependency array contains a value that's recreated fresh on every single render — since React compares dependencies by reference for objects, arrays, and functions, a "new" value every render triggers the effect every render, which can then trigger another render, continuing indefinitely.
The Problem
A screen flickers rapidly, becomes unresponsive, or the app's performance degrades severely, often accompanied by a warning in the console:
Warning: Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a dependency array,
or one of the dependencies changes on every render.
Why It Happens
React's useEffect re-runs whenever any value in its dependency array is different from the previous render — and for objects, arrays, and functions specifically, React uses reference equality, not deep value equality, to determine "different." Common patterns causing this:
- An object or array literal created directly inside the component body and included in a dependency array — even if its actual contents never change, a new literal is a genuinely new reference every single render, so React always sees it as "different"
- A function defined inline inside the component body and passed as a dependency — the same reference-equality issue applies to functions just as much as objects and arrays
- An effect that calls
setStateunconditionally on every run, without any guard condition, combined with a dependency that changes as a direct or indirect result of that same state update, creating a genuine, self-sustaining loop - A prop passed down from a parent component that itself is recreated fresh on every one of the parent's own renders, cascading the "new reference every render" problem down into a child component's effect dependencies
The Fix
For objects or arrays that don't actually need to change between renders, memoize them with useMemo so the same reference is reused unless the underlying values genuinely change:
// Problem: new object reference on every render
const options = { limit: 10, offset: 0 };
useEffect(() => {
fetchData(options);
}, [options]); // triggers every render, since options is always "new"
// Fixed: stable reference unless limit/offset actually change
const options = useMemo(() => ({ limit: 10, offset: 0 }), []);
useEffect(() => {
fetchData(options);
}, [options]);
For functions passed as dependencies, use useCallback similarly, to maintain a stable reference across renders unless its own dependencies genuinely change:
const handleFetch = useCallback(() => {
fetchData();
}, []); // stable reference, doesn't change every render
useEffect(() => {
handleFetch();
}, [handleFetch]);
Where possible, prefer depending on primitive values (strings, numbers, booleans) extracted from an object rather than the object itself, since primitives compare correctly by value rather than by reference, entirely sidestepping the issue:
// Instead of depending on the whole object:
useEffect(() => { ... }, [userObject]);
// Depend on the specific primitive value actually needed:
useEffect(() => { ... }, [userObject.id]);
If an effect genuinely needs to call setState, make sure it does so conditionally, only when the new value actually differs from the current one, rather than unconditionally on every run — this breaks a loop even if a dependency issue exists elsewhere, since an unconditional state update to the same value can still be part of a runaway cycle:
useEffect(() => {
if (newValue !== currentValue) {
setCurrentValue(newValue);
}
}, [newValue, currentValue]);
Still Not Working?
If the loop's source isn't obvious from reviewing the component's own code, use React DevTools' profiler to record what's actually triggering repeated renders, which shows a clear, chronological picture of exactly which state or prop changes are causing each subsequent render — this is significantly more direct than trying to trace the loop purely by reading code, especially when the cycle spans multiple components or involves props passed down from a parent that itself has the underlying issue.