Building a Reusable useFetch Hook in React
Create a custom React hook for data fetching with loading states. Learn how to build reusable hooks that simplify API calls in functional components.
Moshiour Rahman
Advertisement
Overview
Functional components with hooks have become the standard in React development. They provide an elegant way to handle state and side effects with less code than class components.
In this tutorial, we’ll create a reusable useFetch hook that handles data fetching, loading states, and can be used across your entire application.
Project Setup
Create a new React project:
npx create-react-app react-usefetch-demo
cd react-usefetch-demo
Building the useFetch Hook
Create a file src/hooks/useFetch.js:
import { useState, useEffect, useCallback } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
// Return a refetch function for manual refresh
return { data, loading, error, refetch: fetchData };
}
export default useFetch;
What This Hook Does
- Manages three states:
data,loading, anderror - Fetches automatically when the URL changes
- Handles errors gracefully
- Provides a refetch function for manual data refresh
Using the Hook
Basic Example: Photo Gallery
Create src/components/PhotoGallery.jsx:
import React from 'react';
import useFetch from '../hooks/useFetch';
function PhotoGallery() {
const { data: photos, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/photos?albumId=1'
);
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading photos...</p>
</div>
);
}
if (error) {
return (
<div className="error">
<p>Error: {error}</p>
</div>
);
}
return (
<div className="gallery">
<h1>Photo Gallery</h1>
<div className="photo-grid">
{photos.map(({ id, title, thumbnailUrl }) => (
<div key={id} className="photo-card">
<img src={thumbnailUrl} alt={title} />
<p>{title}</p>
</div>
))}
</div>
</div>
);
}
export default PhotoGallery;
Example with Refetch: User List
import React from 'react';
import useFetch from '../hooks/useFetch';
function UserList() {
const { data: users, loading, error, refetch } = useFetch(
'https://jsonplaceholder.typicode.com/users'
);
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<div className="header">
<h1>Users</h1>
<button onClick={refetch}>Refresh</button>
</div>
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong>
<span>{user.email}</span>
</li>
))}
</ul>
</div>
);
}
export default UserList;
Enhanced Version with Options
For more flexibility, here’s an enhanced version with configurable options:
import { useState, useEffect, useCallback, useRef } from 'react';
function useFetch(url, options = {}) {
const {
immediate = true, // Fetch immediately on mount
initialData = null, // Initial data state
} = options;
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(immediate);
const [error, setError] = useState(null);
// Use ref to track if component is mounted
const isMounted = useRef(true);
const fetchData = useCallback(async (fetchOptions = {}) => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
// Only update state if component is still mounted
if (isMounted.current) {
setData(json);
}
} catch (err) {
if (isMounted.current) {
setError(err.message);
}
} finally {
if (isMounted.current) {
setLoading(false);
}
}
}, [url]);
useEffect(() => {
isMounted.current = true;
if (immediate) {
fetchData();
}
return () => {
isMounted.current = false;
};
}, [fetchData, immediate]);
return { data, loading, error, refetch: fetchData };
}
export default useFetch;
Usage with Options
// Fetch immediately (default)
const { data } = useFetch('/api/users');
// Don't fetch immediately - useful for form submissions
const { data, refetch } = useFetch('/api/search', { immediate: false });
// With initial data - prevents flash of empty state
const { data } = useFetch('/api/posts', {
initialData: [],
});
POST/PUT Requests
Extend the hook for mutations:
import { useState, useCallback } from 'react';
function useMutation(url) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const mutate = useCallback(async (data, method = 'POST') => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [url]);
return { mutate, loading, error };
}
export { useMutation };
Usage
function CreateUser() {
const { mutate, loading, error } = useMutation('/api/users');
const handleSubmit = async (formData) => {
try {
const newUser = await mutate(formData);
console.log('Created:', newUser);
} catch (err) {
console.error('Failed to create user');
}
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
Best Practices
- Handle loading and error states - Always show appropriate UI
- Clean up on unmount - Prevent memory leaks
- Use
useCallback- Prevent unnecessary re-renders - Consider caching - For production, use libraries like React Query or SWR
- Type your hooks - Use TypeScript for better developer experience
Conclusion
Custom hooks are one of the most powerful features in React. The useFetch hook we built:
- Encapsulates all fetching logic
- Handles loading and error states
- Is reusable across components
- Can be easily extended
For production applications, consider using established libraries like:
- React Query - Powerful data synchronization
- SWR - Stale-while-revalidate strategy
- RTK Query - If using Redux Toolkit
These provide caching, refetching, and many other features out of the box.
Key Takeaways:
- Custom hooks promote code reuse
- Always handle loading and error states
- Clean up effects to prevent memory leaks
- Consider using battle-tested libraries for production
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Implementing Dark Mode with React Hooks
Learn how to add dark mode functionality to your React application using useState and useEffect hooks with localStorage persistence.
JavaScriptReact Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptTurborepo: High-Performance Monorepo Build System
Master Turborepo for monorepo management. Learn workspace setup, caching, pipelines, and build performant multi-package JavaScript projects.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.