A common performance issue in React occurs when data fetching is improperly placed inside render methods or function components, causing multiple unnecessary network requests. Here’s how to properly structure data fetching to avoid this problem.
The Core Problem
Incorrect implementations that trigger multiple requests:
// ❌ Class component - fetch in render()
class UserProfile extends React.Component {
render() {
fetch('/api/user') // Bad - runs every render
.then(res => res.json())
.then(data => this.setState({ user: data }));
return <div>{this.state?.user?.name}</div>;
}
}
// ❌ Function component - direct fetch
function UserProfile() {
const [user, setUser] = useState(null);
fetch('/api/user') // Bad - runs every render
.then(res => res.json())
.then(setUser);
return <div>{user?.name}</div>;
}
Correct Implementation
1. Class Component – Use componentDidMount
class UserProfile extends React.Component {
state = { user: null };
componentDidMount() {
fetch('/api/user') // ✅ Runs once on mount
.then(res => res.json())
.then(user => this.setState({ user }));
}
render() {
return <div>{this.state.user?.name}</div>;
}
}
2. Function Component – Use useEffect
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user') // ✅ Runs once on mount
.then(res => res.json())
.then(setUser);
}, []); // Empty dependency array = run once
return <div>{user?.name}</div>;
}
Common Mistakes and Solutions
❌ Missing Dependency Array
useEffect(() => {
fetchData(); // ❌ Runs after every render
}); // Missing dependency array
✅ Fix: Add empty dependency array for mount-only
useEffect(() => {
fetchData(); // ✅ Runs once on mount
}, []); // Empty array
❌ Incorrect Dependencies
useEffect(() => {
fetchUser(userId); // ❌ Missing userId dependency
}, []); // Will use stale userId
✅ Fix: Include all dependencies
useEffect(() => {
fetchUser(userId); // ✅ Re-runs when userId changes
}, [userId]); // Proper dependencies
❌ Async Functions Without Cleanup
useEffect(() => {
const fetchData = async () => {
const data = await fetchUser();
setUser(data); // ❌ Potential state update after unmount
};
fetchData();
}, []);
✅ Fix: Add cleanup/cancellation
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
const data = await fetchUser();
if (isMounted) setUser(data); // ✅ Safe update
};
fetchData();
return () => { isMounted = false }; // Cleanup
}, []);
Advanced Patterns
1. Custom Hook for Data Fetching
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(response.statusText);
const json = await response.json();
if (isMounted) {
setData(json);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setData(null);
}
} finally {
if (isMounted) setLoading(false);
}
};
fetchData();
return () => {
isMounted = false;
};
}, [url, options]);
return { data, error, loading };
}
// Usage
function UserProfile() {
const { data: user, error, loading } = useFetch('/api/user');
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <div>{user.name}</div>;
}
2. Cancellation with AbortController
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/user', {
signal: controller.signal
});
const user = await response.json();
setUser(user);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
}
};
fetchData();
return () => controller.abort(); // Cancel on unmount
}, []);
3. Debouncing Frequent Changes
useEffect(() => {
const timerId = setTimeout(() => {
fetchResults(searchQuery);
}, 300); // Wait 300ms after last change
return () => clearTimeout(timerId); // Cleanup previous timer
}, [searchQuery]); // Runs when searchQuery changes
Best Practices
- Fetch in useEffect (function components) or componentDidMount (class components)
- Include proper dependencies in useEffect
- Implement cleanup to prevent memory leaks
- Handle loading/error states for better UX
- Consider custom hooks for reusable fetch logic
- Cancel pending requests on unmount
- Debounce rapid changes for search/input fields
Key Takeaways
- Never place fetches in render – they’ll run too often
- Use lifecycle methods appropriately for data fetching
- Handle component unmounting to prevent memory leaks
- Consider edge cases like slow networks and errors
- Optimize performance with debouncing and caching
Proper data fetching architecture prevents performance issues and ensures your React components behave predictably when loading asynchronous data.
