JAVASCRIPT
Build a Reusable `useFetch` Hook for Data Loading in React
Build a reusable `useFetch` hook for React. Encapsulate data fetching logic, including loading, error handling, and data states, for declarative and efficient API calls.
import { useState, useEffect, useRef } from 'react';
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const optionsRef = useRef(options); // To prevent infinite loop if options object is not memoized externally
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { ...optionsRef.current, signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (e) {
if (e.name === 'AbortError') {
console.log('Fetch aborted');
return;
}
setError(e);
setData(null);
} finally {
setLoading(false);
}
};
if (url) {
fetchData();
}
return () => {
abortController.abort(); // Cleanup: abort ongoing fetch request
};
}, [url]); // Re-run effect if URL changes
return { data, loading, error };
}
// Example Usage:
function DataDisplay() {
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts/1');
const {
data: todoData,
loading: todoLoading,
error: todoError
} = useFetch('https://jsonplaceholder.typicode.com/todos/1');
const containerStyle = {
padding: '20px',
border: '1px solid #eee',
borderRadius: '8px',
maxWidth: '600px',
margin: '20px auto',
textAlign: 'left'
};
const sectionStyle = {
marginBottom: '20px',
borderBottom: '1px dashed #ccc',
paddingBottom: '15px'
};
if (loading || todoLoading) return <div style={containerStyle}>Loading data...</div>;
if (error) return <div style={containerStyle}><p style={{color: 'red'}}>Error: {error.message}</p></div>;
if (todoError) return <div style={containerStyle}><p style={{color: 'red'}}>Error fetching todo: {todoError.message}</p></div>;
return (
<div style={containerStyle}>
<h2>Data from API Calls:</h2>
<div style={sectionStyle}>
<h3>Post Data:</h3>
{data ? (
<>
<p><strong>Title:</strong> {data.title}</p>
<p><strong>Body:</strong> {data.body}</p>
</>
) : (
<p>No post data available.</p>
)}
</div>
<div style={sectionStyle}>
<h3>Todo Data:</h3>
{todoData ? (
<>
<p><strong>Title:</strong> {todoData.title}</p>
<p><strong>Completed:</strong> {todoData.completed ? 'Yes' : 'No'}</p>
</>
) : (
<p>No todo data available.</p>
)}
</div>
</div>
);
}
export default DataDisplay;
How it works: This custom `useFetch` hook encapsulates the logic for asynchronous data fetching, managing loading, error, and success states. It leverages `useEffect` to initiate the fetch when the component mounts or the `url` dependency changes, and includes an `AbortController` for proper cleanup to prevent memory leaks or state updates on unmounted components. `useRef` is used for `options` to ensure that if the options object isn't memoized externally, it doesn't cause an infinite loop in the `useEffect`'s dependency array.