Introduction
Custom hooks in React enable you to extract reusable logic — such as making API requests — into neat, reusable functions. This makes your components cleaner, easier to read, and simpler to test. In this guide, you'll learn how to create safe, efficient, and reusable custom hooks for fetching data from APIs in React.
Why Use Custom Hooks for API Calls?
Reusability — Put common API logic (fetching, loading state, error handling) in one place and reuse it across components.
Cleaner Components — Components become mostly about UI; data fetching moves into hooks.
Testability — Hooks are easy to test independently from UI.
Consistency — One hook enforces a consistent pattern for all API calls (caching, retries, aborting).
Using custom hooks helps teams, especially developers in India, the USA, and the UK, keep large React apps maintainable and fast.
Basic Pattern: useFetch
Hook
A simple reusable hook for GET requests. It manages three states: data
, loading
, and error
.
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
let isMounted = true; // avoid state update after unmount
async function fetchData() {
setLoading(true);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const json = await res.json();
if (isMounted) setData(json);
} catch (err) {
if (isMounted) setError(err);
} finally {
if (isMounted) setLoading(false);
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
How to use it in a component:
import React from 'react';
import { useFetch } from './hooks/useFetch';
function Posts() {
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default Posts;
Advanced Hook: useApi
with POST, cancelation, and headers
For real apps you often need more control: custom headers, POST requests, request cancellation (AbortController), and manual triggers.
import { useState, useRef, useEffect, useCallback } from 'react';
export function useApi(defaultUrl = '', defaultOptions = {}) {
const [url, setUrl] = useState(defaultUrl);
const [options, setOptions] = useState(defaultOptions);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const abortCtrlRef = useRef(null);
const callApi = useCallback(async (callUrl = url, callOptions = options) => {
if (!callUrl) return;
setLoading(true);
setError(null);
abortCtrlRef.current?.abort(); // abort previous
const controller = new AbortController();
abortCtrlRef.current = controller;
try {
const res = await fetch(callUrl, { signal: controller.signal, ...callOptions });
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const json = await res.json();
setData(json);
return json;
} catch (err) {
if (err.name === 'AbortError') {
// request was cancelled
return;
}
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
// optional: auto-call when url changes
if (url) callApi();
return () => abortCtrlRef.current?.abort();
}, [url, callApi]);
return {
data,
loading,
error,
callApi,
setUrl,
setOptions,
};
}
Example usage for POST:
function CreatePost() {
const { data, loading, error, callApi } = useApi();
const handleSubmit = async (formData) => {
try {
const json = await callApi('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
console.log('Created', json);
} catch (err) {
console.error(err);
}
};
return (
<div>
{/* form code */}
{loading && <p>Creating post...</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}
Using useSWR
or React Query
Instead of Building From Scratch
For caching, background revalidation, retries, pagination, and more advanced features, consider libraries:
These libraries handle caching and revalidation for you with small APIs like useSWR(url, fetcher)
or useQuery
from React Query. They are great for production apps and large teams.
Example with SWR:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function UserList() {
const { data, error } = useSWR('https://jsonplaceholder.typicode.com/users', fetcher);
if (!data) return <div>Loading...</div>;
if (error) return <div>Error</div>;
return (
<ul>
{data.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Tips for Robust API Hooks
Handle Abort / Cancelation: Prevent state updates after unmount and cancel slow requests using AbortController
.
Error Handling: Provide helpful error messages and consider retry logic for transient failures.
Caching: Cache results where useful to reduce network calls.
Debounce Inputs: When calling APIs from user input, debounce to avoid too many requests.
Authentication: Centralize auth headers (tokens) and refresh flows in the hook or a wrapper.
Environment Variables: Use process.env
for base URLs to handle different environments (development, production).
Testing: Mock fetch in tests with tools like msw
(Mock Service Worker) to test hooks without real network calls.
Accessibility and SEO Notes
For API-driven content that affects SEO (server-rendered lists, product pages), prefer server-side rendering (SSR) or static generation (SSG) in frameworks like Next.js. Client-side hooks are great for user dashboards and interactive UIs but may not be crawled by search engines as easily.
Ensure loading states and error messages are accessible (ARIA) and provide useful content for screen readers.
Summary
Custom hooks in React simplify and standardize how your app interacts with APIs. Start with a simple useFetch
for GET requests, then move to a more flexible useApi
for POSTs, headers, and cancellation. For advanced needs, use battle-tested libraries like SWR or React Query. Follow best practices like aborting requests, handling errors, caching, and keeping environment-specific URLs in process.env
for cleaner deployments across India, USA, UK, and other regions.