Creating Reusable Data Fetching Hooks in React Applications
Custom hooks encapsulate reusable stateful logic, making components cleaner and promoting code reuse. For data fetching, a well-designed hook handles loading states, errors, caching, and request deduplicationācommon concerns across API calls. Instead of repeating useEffect, useState, and try/catch blocks in every component, abstract this pattern into a hook like useApi. This approach improves consistency, reduces boilerplate, and centralizes error handling and retry logic. Key considerations include stabilizing dependencies, managing race conditions, and integrating with React Query or SWR if adopted later. For financial or e-commerce apps where data reliability impacts user trust, such hooks ensure uniform behavior: showing skeletons on load, displaying helpful error messages, and preventing stale UI updates. They also facilitate testingāyou can unit test the hook in isolation with mocked fetch implementations. Treat custom hooks as shared utilities; document their contracts clearly and version them if shared across projects.
š»Source Code
// hooks/useApi.js
import { useState, useEffect, useCallback, useRef } from "react";
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [lastUrl, setLastUrl] = useState(null);
// Ref to track if component is mounted (avoid state updates on unmounted)
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const fetchData = useCallback(async () => {
if (!url) return;
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
// Only update state if component still mounted and URL hasn't changed
if (isMounted.current && url === lastUrl) {
setData(result);
setLoading(false);
}
} catch (err) {
if (isMounted.current && url === lastUrl) {
setError(err.message);
setLoading(false);
}
}
}, [url, options, lastUrl]);
// Reset when URL changes
useEffect(() => {
setLastUrl(url);
fetchData();
}, [url, fetchData]);
// Expose refetch function for manual triggers
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
export default useApi;
// Usage example in a component
// ProductCard.jsx
import React from "react";
import useApi from "../hooks/useApi";
export default function ProductCard({ productId }) {
const { data: product, loading, error } = useApi(
`/api/products/${productId}`
);
if (loading) return <div className="skeleton-loader">Loading...</div>;
if (error) return <div className="error">Failed to load product</div>;
return (
<div className="product-card">
<h3>{product?.name}</h3>
<p>${product?.price?.toFixed(2)}</p>
<button>Add to cart</button>
</div>
);
}
// Example with custom configuration (headers, method)
// useApi("/api/user/profile", { headers: { Authorization: `Bearer ${token}`} })šRelated Snippets
Similar code snippets you might find interesting
Using Intersection Observer for Lazy Loading and Scroll-Based Animations
Leveraging Astro Islands for Interactive Content-Heavy Sites
Maintaining a Simple Global State Store in React Without Extra Libraries
š¬Comments (0)
šPlease login to post comments
No comments yet. Be the first to share your thoughts!
ā”Actions
Share this snippet:
š¤About the Author
admin
Active contributor
