Preventing Infinite Loops in useEffect with Proper Dependencies
A common React mistake is creating infinite loops in useEffect
by specifying incorrect dependencies, causing the effect to repeatedly trigger itself.
The Problem (Infinite Loop)
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// ❌ Triggers infinite re-renders
setLoading(true);
fetchUser().then(data => {
setUser(data);
setLoading(false);
});
}, [user, loading]); // Problematic dependencies
return <div>{loading ? 'Loading...' : user?.name}</div>;
}
Why this causes an infinite loop:
- Effect runs when
user
orloading
changes - Effect modifies both
user
andloading
- Triggers another effect run
- Cycle repeats indefinitely
Correct Solutions
1. Empty Dependency Array (Mount Only)
useEffect(() => {
setLoading(true);
fetchUser().then(data => {
setUser(data);
setLoading(false);
});
}, []); // ✅ Runs only once on mount
2. Proper State Management
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
const data = await fetchUser();
setUser(data);
setLoading(false);
}, []);
useEffect(() => {
fetchData();
}, [fetchData]); // ✅ Stable dependency
3. Using Refs for Tracking
const isMounted = useRef(false);
useEffect(() => {
if (!isMounted.current) {
setLoading(true);
fetchUser().then(data => {
setUser(data);
setLoading(false);
});
isMounted.current = true;
}
}, []); // ✅ Runs only once
4. For Dependent Effects
const [userId, setUserId] = useState(1);
useEffect(() => {
let ignore = false;
setLoading(true);
fetchUser(userId).then(data => {
if (!ignore) {
setUser(data);
setLoading(false);
}
});
return () => { ignore = true }; // ✅ Cleanup for rapid ID changes
}, [userId]); // ✅ Only re-run when userId changes
Common Infinite Loop Scenarios
- State setters in dependency array:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count, setCount]); // ❌ Infinite updates
- New objects/arrays as dependencies:
const [data, setData] = useState([]);
useEffect(() => {
setData([...data, newItem]);
}, [data]); // ❌ New array reference each time
- Functions without useCallback:
const fetch = () => { /*...*/ };
useEffect(() => {
fetch();
}, [fetch]); // ❌ New function each render
Best Practices
- Be precise with dependencies – Only include what’s actually used
- Memoize objects/functions with
useMemo
/useCallback
when needed in deps - Consider derived state – Often better than effects
- Use the exhaustive-deps ESLint rule to catch issues
- Break complex effects into smaller, focused effects
Advanced Patterns
1. Effect Dependencies with Refs
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // Track latest value
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // Always current
}, 1000);
return () => clearInterval(timer);
}, []); // No count dependency needed
2. Custom Hook for Data Fetching
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(res => res.json())
.then(data => !ignore && setData(data));
return () => { ignore = true };
}, [url]); // Only re-run when URL changes
return data;
}
3. Combining Multiple States
const [state, setState] = useState({
user: null,
loading: false,
error: null
});
useEffect(() => {
let isMounted = true;
setState(prev => ({...prev, loading: true}));
fetchUser()
.then(user => isMounted && setState({user, loading: false, error: null}))
.catch(error => isMounted && setState(prev => ({...prev, loading: false, error})));
return () => { isMounted = false };
}, []); // Single effect