Most common pitfalls, why they happen, and how to avoid them:
⚠️ 1. Not Handling Loading and Error States
❌ Problem:
Developers often assume data will always load successfully and forget to show a proper loading or error message.
// Bad: assumes API always succeeds
const [data, setData] = useState();
useEffect(() => {
fetch("/api/data").then(res => res.json()).then(setData);
}, []);
✅ Fix:
Always track loading and error states to keep UI predictable.
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error("Failed to fetch");
const result = await res.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
⚠️ 2. Ignoring Cleanup When Component Unmounts
❌ Problem:
If the user navigates away before the fetch completes, you may get:
⚠️ "Can't perform a React state update on an unmounted component"
✅ Fix:
Use a flag or an AbortController to cancel requests.
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch("/api/data", { signal: controller.signal });
const data = await res.json();
setData(data);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
}
}
fetchData();
return () => controller.abort();
}, []);
⚠️ 3. Re-Fetching on Every Render (Missing Dependency Array)
❌ Problem:
If you forget the dependency array in useEffect, the fetch runs on every render, causing infinite loops.
useEffect(() => {
fetchData();
}); // ❌ no dependency array
✅ Fix:
Add an empty array [] to run only once, or proper dependencies to control re-fetching.
useEffect(() => {
fetchData();
}, []); // ✅ runs once
⚠️ 4. Not Memoizing Callbacks or Derived Data
If you pass an inline function or object to a dependency, useEffect thinks it changed every time → repeated API calls.
useEffect(() => {
fetchData(filters); // ❌ `filters` recreated each render
}, [filters]);
✅ Fix:
Memoize filters or callbacks using useMemo or useCallback.
const stableFilters = useMemo(() => filters, [filters.id]);
useEffect(() => fetchData(stableFilters), [stableFilters]);
⚠️ 5. Fetching Data in Render or Conditional Blocks
Never call fetch() or async functions directly inside the component body — it causes multiple fetches per render.
❌ Problem:
function App() {
const res = fetch("/api/data"); // ❌ runs every render
}
✅ Fix:
Use useEffect for side effects.
⚠️ 6. Over-Fetching or Unnecessary Re-fetching
Re-fetching on every small change wastes bandwidth and causes flickering.
✅ Use caching libraries like:
- React Query
- SWR
- Apollo Client
They handle caching, revalidation, and deduplication automatically.
⚠️ 7. Not Handling Slow or Partial Responses
If APIs are slow, users might see blank screens.
✅ Always show skeleton loaders or spinners during fetch.
if (loading) return <Skeleton count={5} />;
⚠️ 8. Missing Error Boundaries for API Failures
Errors during data rendering (e.g., undefined fields) can break the whole app.
✅ Use Error Boundaries to safely catch and display fallback UI.
⚠️ 9. Mixing Async Logic and Rendering Logic
Putting all logic in the component makes it messy and hard to maintain.
✅ Fix: Separate data fetching into custom hooks.
function usePosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("/api/posts").then(res => res.json()).then(setPosts);
}, []);
return posts;
}
⚠️ 10. Ignoring Performance Impacts of Large Data
Rendering huge lists after fetching can lag.
✅ Use:
- Pagination
- Infinite scroll (
react-window,react-virtualized) - Lazy loading
🧱 Summary
| Pitfall | Why it’s a problem | Fix |
|---|---|---|
| No loading/error handling | Poor UX | Add loading and error states |
| No cleanup | Memory leaks | Use AbortController |
| Missing dependency array | Infinite loops | Add [] |
| Inline objects/functions | Unnecessary re-fetch | Memoize |
| Fetching in render | Multiple fetches | Use useEffect |
| Over-fetching | Wasted network calls | Cache with React Query / SWR |
| No skeleton loaders | Blank screens | Add placeholders |
| Missing error boundaries | Crashes on bad data | Wrap with Error Boundary |
| Large data rendering | Performance issues | Use virtualization |
✅ In short:
Handle loading and error states, clean up requests, control dependencies, and prefer caching libraries like React Query or SWR for reliable, performant data fetching.