![]()
Avoiding Multiple Data Fetches in React Components
A common React anti-pattern is placing data fetching logic directly in the render method or function component body, which can cause:
- Infinite request loops
- Performance issues
- API rate limit problems
- Inconsistent UI state
The Problem (Incorrect Fetching)
// ❌ Wrong - Fetches on every render
function UserProfile() {
const [user, setUser] = useState(null);
// Bad - runs on every render
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
return <div>{user?.name}</div>;
}
Correct Solutions
1. useEffect with Empty Dependencies (Recommended)
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// ✅ Runs once on mount
fetch('/api/user')
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(data => setUser(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []); // Empty array = run once
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{user.name}</div>;
}
2. Using AbortController for Cleanup
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch('/api/user', {
signal: controller.signal
});
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
}
};
fetchData();
return () => controller.abort(); // ✅ Cleanup on unmount
}, []);
3. With Dependency Tracking
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
// ✅ Re-fetches only when userId changes
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(setPosts);
}, [userId]); // Dependency array
return <PostList posts={posts} />;
}
Common Mistakes to Avoid
- Forgetting dependency arrays:
useEffect(() => {
fetchData(); // ❌ Runs on every render
}); // Missing dependency array
- Incorrect dependencies:
useEffect(() => {
fetchData(id);
}, []); // ❌ Missing id dependency
- Async functions directly in useEffect:
useEffect(async () => { // ❌ Can't return cleanup from async
const data = await fetchData();
setData(data);
}, []);
- Fetching in event handlers without state:
function Search() {
const handleSearch = (query) => {
fetchResults(query); // ❌ Should store results in state
};
return <input onChange={e => handleSearch(e.target.value)} />;
}
Best Practices
- Always use useEffect for data fetching
- Include proper dependencies – Empty array for mount-only
- Handle loading/error states – Don’t assume requests succeed
- Clean up requests with AbortController
- Memoize expensive operations with useMemo
- Consider custom hooks for reusable fetching logic:
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch(url, {
...options,
signal: controller.signal
});
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') setError(err);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]); // Re-run when URL changes
return { data, error, loading };
}
// Usage
function UserProfile() {
const { data: user, loading, error } = useFetch('/api/user');
// Render based on state
}
Advanced Patterns
1. Request Deduplication
const fetchCache = new Map();
function useFetchDeduplicated(url) {
const [state, setState] = useState({ data: null, loading: true });
useEffect(() => {
if (fetchCache.has(url)) {
setState({ data: fetchCache.get(url), loading: false });
return;
}
fetch(url)
.then(res => res.json())
.then(data => {
fetchCache.set(url, data);
setState({ data, loading: false });
});
}, [url]);
return state;
}
2. Optimistic Updates
function TodoList() {
const [todos, setTodos] = useState([]);
const toggleTodo = async (id) => {
const oldTodos = todos;
// Optimistically update UI
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
try {
await fetch(`/api/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ completed: !oldTodos.find(t => t.id === id).completed })
});
} catch (err) {
// Revert on error
setTodos(oldTodos);
showErrorToast('Failed to update');
}
};
}
Remember: Data fetching should be:
- Intentional – Not triggered by renders
- Controlled – With cleanup mechanisms
- Traceable – With proper loading/error states
- Efficient – Avoiding duplicate requests
Always place data fetching logic in useEffect hooks or event handlers, never directly in the render flow.
