use-fetch-with-callbacks
Version:
A powerful React hook for HTTP requests with comprehensive callback support, request chaining, and TypeScript integration
412 lines (411 loc) • 17.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
/**
* A React hook that provides fetch functionality with callback support for success, error, and loading states
*
* @template T - The type of data expected from the API response
* @param endpoint - The API endpoint path to make requests to
* @param options - Optional configuration including base URL and headers
* @returns An object containing response data, loading state, error state, and request methods
*
* @example
* Basic usage:
* ```typescript
* const { response, loading, error, fetchData } = useFetchWithCallbacks<User>('/users/1', {
* baseUrl: 'https://api.example.com',
* headers: { 'Authorization': 'Bearer token' }
* });
*
* // Fetch data with callbacks
* fetchData(
* (data) => console.log('Success:', data),
* (error) => console.error('Error:', error),
* (loading) => console.log('Loading:', loading)
* );
* ```
*
* @example
* Chaining operations:
* ```typescript
* const { chain } = useFetchWithCallbacks<User>('/users/1', {
* baseUrl: 'https://api.example.com'
* });
*
* // Chain multiple operations
* chain()
* .fetch((data) => console.log('Fetched:', data))
* .post({ name: 'John' }, (data) => console.log('Posted:', data))
* .put({ name: 'Jane' }, (data) => console.log('Updated:', data))
* .then((finalData) => console.log('All operations completed:', finalData))
* .catch((error) => console.error('Chain failed:', error))
* .finally(() => console.log('Chain finished'))
* .execute();
* ```
*/
const useFetchWithCallbacks = (endpoint, options) => {
var _a, _b;
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [requestCompleted, setRequestCompleted] = useState(false);
const abortControllerRef = useRef(null);
const baseUrl = (_a = options === null || options === void 0 ? void 0 : options.baseUrl) !== null && _a !== void 0 ? _a : '';
const timeout = (_b = options === null || options === void 0 ? void 0 : options.timeout) !== null && _b !== void 0 ? _b : 10000;
const headers = useMemo(() => (Object.assign({ 'Content-Type': 'application/json' }, options === null || options === void 0 ? void 0 : options.headers)), [options === null || options === void 0 ? void 0 : options.headers]);
const setLoadingState = (isLoading, onLoading) => {
setLoading(isLoading);
if (onLoading) {
onLoading(isLoading);
}
};
const executeMultipleRequest = useCallback((fetchFn, onSuccess, onError, onLoading) => __awaiter(void 0, void 0, void 0, function* () {
// Cancel any ongoing request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoadingState(true, onLoading);
setError(null);
setRequestCompleted(false);
try {
const data = yield fetchFn(abortController.signal);
// Check if request was aborted
if (abortController.signal.aborted) {
return;
}
setResponse(data);
setRequestCompleted(true);
if (onSuccess) {
onSuccess(data);
}
}
catch (err) {
// Don't set error if request was aborted
if (abortController.signal.aborted) {
return;
}
const errorObj = err instanceof Error ? err : new Error(String(err));
setError(errorObj);
if (onError) {
onError(errorObj);
}
}
finally {
if (!abortController.signal.aborted) {
setLoadingState(false, onLoading);
}
}
}), [timeout]);
const executeRequest = useCallback((fetchFn, onSuccess, onError, onLoading) => __awaiter(void 0, void 0, void 0, function* () {
// Cancel any ongoing request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoadingState(true, onLoading);
setError(null);
setRequestCompleted(false);
try {
const data = yield fetchFn(abortController.signal);
// Check if request was aborted
if (abortController.signal.aborted) {
return;
}
setResponse(data);
setRequestCompleted(true);
if (onSuccess) {
onSuccess(data);
}
}
catch (err) {
// Don't set error if request was aborted
if (abortController.signal.aborted) {
return;
}
const errorObj = err instanceof Error ? err : new Error(String(err));
setError(errorObj);
if (onError) {
onError(errorObj);
}
}
finally {
if (!abortController.signal.aborted) {
setLoadingState(false, onLoading);
}
}
}), []);
// Cleanup effect to abort requests when component unmounts
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
const fetchData = useCallback((onSuccess, onError, onLoading) => executeRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${endpoint}`;
const res = yield Promise.race([
fetch(url, { method: 'GET', headers, signal: abortSignal }),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), onSuccess, onError, onLoading), [baseUrl, endpoint, headers, executeRequest, timeout]);
const postData = useCallback((data, onSuccess, onError, onLoading) => executeRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${endpoint}`;
const res = yield Promise.race([
fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(data),
signal: abortSignal,
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), onSuccess, onError, onLoading), [baseUrl, endpoint, headers, executeRequest, timeout]);
const putData = useCallback((data, onSuccess, onError, onLoading) => executeRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${endpoint}`;
const res = yield Promise.race([
fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(data),
signal: abortSignal,
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), onSuccess, onError, onLoading), [baseUrl, endpoint, headers, executeRequest, timeout]);
const deleteData = useCallback((onSuccess, onError, onLoading) => executeRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${endpoint}`;
const res = yield Promise.race([
fetch(url, {
method: 'DELETE',
headers,
signal: abortSignal,
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), onSuccess, onError, onLoading), [baseUrl, endpoint, headers, executeRequest, timeout]);
const patchData = useCallback((data, onSuccess, onError, onLoading) => executeRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${endpoint}`;
const res = yield Promise.race([
fetch(url, {
method: 'PATCH',
headers,
body: JSON.stringify(data),
signal: abortSignal,
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), onSuccess, onError, onLoading), [baseUrl, endpoint, headers, executeRequest, timeout]);
const fetchMultipleData = useCallback((endpoints, onSuccess, onError, onLoading) => executeMultipleRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const promises = endpoints.map((ep) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${ep}`;
const res = yield Promise.race([
fetch(url, { method: 'GET', headers, signal: abortSignal }),
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
]);
if (!res.ok) {
const errorText = yield res.text();
let errorMessage = 'An error occurred';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.message || errorMessage;
}
catch (_a) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
return res.json();
}));
return Promise.all(promises);
}), onSuccess, onError, onLoading), [baseUrl, headers, executeMultipleRequest, timeout]);
const chain = useCallback(() => {
const operations = [];
let finalSuccessCallback;
let finalErrorCallback;
let finallyCallback;
const chainableRequest = {
fetch: (onSuccess, onError, onLoading) => {
operations.push({ type: 'GET', onSuccess, onError, onLoading });
return chainableRequest;
},
post: (data, onSuccess, onError, onLoading) => {
operations.push({ type: 'POST', data, onSuccess, onError, onLoading });
return chainableRequest;
},
put: (data, onSuccess, onError, onLoading) => {
operations.push({ type: 'PUT', data, onSuccess, onError, onLoading });
return chainableRequest;
},
delete: (onSuccess, onError, onLoading) => {
operations.push({ type: 'DELETE', onSuccess, onError, onLoading });
return chainableRequest;
},
patch: (data, onSuccess, onError, onLoading) => {
operations.push({ type: 'PATCH', data, onSuccess, onError, onLoading });
return chainableRequest;
},
then: callback => {
finalSuccessCallback = callback;
return chainableRequest;
},
catch: callback => {
finalErrorCallback = callback;
return chainableRequest;
},
finally: callback => {
finallyCallback = callback;
return chainableRequest;
},
execute: () => __awaiter(void 0, void 0, void 0, function* () {
let lastResult = null;
try {
for (const operation of operations) {
yield new Promise((resolve, reject) => {
const onSuccess = (data) => {
lastResult = data;
if (operation.onSuccess) {
operation.onSuccess(data);
}
resolve();
};
const onError = (error) => {
if (operation.onError) {
operation.onError(error);
}
reject(error);
};
switch (operation.type) {
case 'GET':
fetchData(onSuccess, onError, operation.onLoading);
break;
case 'POST':
postData(operation.data, onSuccess, onError, operation.onLoading);
break;
case 'PUT':
putData(operation.data, onSuccess, onError, operation.onLoading);
break;
case 'DELETE':
deleteData(onSuccess, onError, operation.onLoading);
break;
case 'PATCH':
patchData(operation.data, onSuccess, onError, operation.onLoading);
break;
}
});
}
if (finalSuccessCallback && lastResult) {
finalSuccessCallback(lastResult);
}
}
catch (error) {
if (finalErrorCallback) {
finalErrorCallback(error);
}
}
finally {
if (finallyCallback) {
finallyCallback();
}
}
}),
};
return chainableRequest;
}, [fetchData, postData, putData, deleteData, patchData]);
return {
response,
loading,
error,
requestCompleted,
fetchData,
postData,
putData,
deleteData,
patchData,
fetchMultipleData,
chain,
};
};
export default useFetchWithCallbacks;