🧭 Introduction
Almost every real-world React application talks to an API.
But many codebases suffer from:
- Repeated
useEffect+fetchlogic - Scattered loading and error handling
- Inconsistent API behavior across components
This leads to:
❌ Messy components
❌ Hard-to-debug bugs
❌ Poor reusability
The professional solution is to extract API logic into custom hooks.
In this lesson, you’ll learn how to design clean, reusable API hooks using patterns like useFetch and useAsync.
🎯 What You’ll Learn in This Lesson
By the end of this lesson, you will understand:
- Why API logic does not belong in components
- How to design a reusable API hook
- Managing loading, error, and data states
- Handling dependency changes safely
- Avoiding common API hook mistakes
❓ Why API Logic Should NOT Live in Components
❌ Typical Component Code
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
Problems:
- Repeated everywhere
- Hard to test
- Clutters UI code
👉 Components should focus on what to render, not how data is fetched.
🧠 Mental Model (Very Important)
Components = UI
Hooks = Behavior / Logic
API calls are behavior, not UI.
🧩 Designing a Good API Hook (Checklist)
A good API hook should:
✅ Be reusable
✅ Accept parameters (URL, options)
✅ Manage loading, error, and data
✅ Handle dependency changes
✅ Clean up properly
❌ Not assume UI structure
🧪 Building useFetch (Step-by-Step)
Step 1: Basic Structure
import { useEffect, useState } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then(res => {
if (!res.ok) throw new Error("Request failed");
return res.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
🔍 Using useFetch
function Users() {
const { data, loading, error } = useFetch("/api/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
👉 Component stays clean and declarative.
⚠ Handling Dependency Changes Correctly
Whenever url changes:
- Effect re-runs
- Data refetches
This is expected and desired.
⚠️ Never suppress dependencies just to “fix warnings”.
🚨 Avoiding Race Conditions (Advanced Insight)
If the URL changes quickly:
- Older requests may resolve later
- State may update incorrectly
🔍 Safe Pattern with AbortController
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== "AbortError") {
setError(err);
}
});
return () => controller.abort();
}, [url]);
👉 Prevents stale updates and memory leaks.
🔁 Introducing useAsync (More Flexible Pattern)
useFetch works well for GET requests, but real apps need:
- POST
- PUT
- DELETE
- Manual triggers
This is where useAsync shines.
🧩 Building useAsync Hook
import { useCallback, useState } from "react";
function useAsync(asyncFunction) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const execute = useCallback(async (...args) => {
setLoading(true);
setError(null);
try {
const result = await asyncFunction(...args);
setData(result);
return result;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [asyncFunction]);
return { execute, data, loading, error };
}
🔍 Using useAsync
const fetchUsers = () => fetch("/api/users").then(res => res.json());
function Users() {
const { execute, data, loading, error } = useAsync(fetchUsers);
useEffect(() => {
execute();
}, [execute]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <div>{JSON.stringify(data)}</div>;
}
👉 Powerful, flexible, reusable.
🆚 useFetch vs useAsync (Decision Guide)
| Scenario | Use |
|---|---|
| Simple GET request | useFetch |
| Manual trigger | useAsync |
| POST / PUT / DELETE | useAsync |
| Complex workflows | useAsync |
🚨 Common Mistakes with API Hooks
❌ Mistake 1: Hardcoding URLs
Always pass URL or function as parameter.
❌ Mistake 2: No Cleanup
Leads to memory leaks.
❌ Mistake 3: Mixing UI Logic
Hooks should not return JSX.
❌ Mistake 4: Ignoring Errors
Always expose error state.
🧠 Real-World Insight
In large apps:
- Custom hooks handle client-side API logic
- Libraries like React Query handle server state
You must understand both.
🎯 Best Practices (Senior-Level)
✅ Keep hooks generic
✅ Expose predictable APIs
✅ Handle loading and errors
✅ Abort stale requests
✅ Prefer composition over duplication
❓ FAQs — API Custom Hooks
🔹 Should every API call use a custom hook?
Yes, if logic repeats or grows complex.
🔹 Can hooks replace Axios interceptors?
No — they complement each other.
🔹 Is useFetch enough for production apps?
For small apps — yes.
For large apps — consider React Query.
🔹 Should hooks handle retries?
Sometimes — but keep responsibility clear.
🧠 Quick Recap
✔ API logic belongs in hooks
✔ useFetch handles simple GET requests
✔ useAsync handles flexible async flows
✔ Cleanup prevents bugs
✔ Clean APIs scale better
🎉 Conclusion
Well-designed API hooks are a game-changer.
Once you master them:
- Components stay clean
- Logic becomes reusable
- Apps scale gracefully
This lesson completes your mastery of API-based custom hooks ⚛️🧠
👉 Next Lesson
Lesson 12 — Authentication Custom Hook (useAuth)