107 lines
2.7 KiB
JavaScript
107 lines
2.7 KiB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
function useFetch(url, options = {}) {
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const abortControllerRef = useRef(null);
|
|
|
|
const {
|
|
method = 'GET',
|
|
headers = {},
|
|
body = null,
|
|
skip = false, // Don't auto-fetch on mount
|
|
...restOptions
|
|
} = options;
|
|
|
|
const fetchData = useCallback(async (dynamicBody = null, dynamicOptions = {}) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Cancel previous request
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
}
|
|
|
|
abortControllerRef.current = new AbortController();
|
|
|
|
try {
|
|
const finalBody = dynamicBody || body;
|
|
const finalMethod = dynamicOptions.method || method;
|
|
|
|
const config = {
|
|
method: finalMethod,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...headers,
|
|
...dynamicOptions.headers
|
|
},
|
|
signal: abortControllerRef.current.signal,
|
|
...restOptions,
|
|
...dynamicOptions
|
|
};
|
|
|
|
// Add body for POST, PUT, PATCH, DELETE (if needed)
|
|
if (finalBody && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(finalMethod)) {
|
|
config.body = typeof finalBody === 'string'
|
|
? finalBody
|
|
: JSON.stringify(finalBody);
|
|
}
|
|
|
|
const response = await fetch(url, config);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Handle empty responses (like 204 No Content)
|
|
const contentType = response.headers.get('content-type');
|
|
let result = null;
|
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
result = await response.json();
|
|
} else if (response.status !== 204) {
|
|
result = await response.text();
|
|
}
|
|
|
|
setData(result);
|
|
setError(null);
|
|
return result;
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') {
|
|
console.log('Fetch aborted');
|
|
} else {
|
|
setError(err.message);
|
|
throw err;
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [url, method, body, JSON.stringify(headers), JSON.stringify(restOptions)]);
|
|
|
|
useEffect(() => {
|
|
if (!skip && method === 'GET') {
|
|
fetchData();
|
|
}
|
|
|
|
return () => {
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
}
|
|
};
|
|
}, [fetchData, skip, method]);
|
|
|
|
const refetch = useCallback(() => {
|
|
return fetchData();
|
|
}, [fetchData]);
|
|
|
|
return {
|
|
data,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
execute: fetchData // Alias for manual calls
|
|
};
|
|
}
|
|
|
|
export default useFetch; |