use-fetch-with-callbacks
Version:
A powerful React hook for HTTP requests with comprehensive callback support, request chaining, and TypeScript integration
453 lines (452 loc) • 20.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 with multiple endpoints:
* ```typescript
* const { chain } = useFetchWithCallbacks<User>('/users/1', {
* baseUrl: 'https://api.example.com'
* });
*/
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]);
/**
* Sets the loading state and calls the onLoading callback if provided.
*
* @param isLoading - The new loading state
* @param onLoading - Optional callback to be called with the new loading state
*/
const setLoadingState = (isLoading, onLoading) => {
setLoading(isLoading);
if (onLoading) {
onLoading(isLoading);
}
};
/**
* Executes a single API request and manages loading, error, and success states.
*
* @param fetchFn - Function that performs the fetch and returns a Promise of T
* @param onSuccess - Optional callback for successful response
* @param onError - Optional callback for error response
* @param onLoading - Optional callback for loading state
* @returns Promise<void>
*/
const executeRequest = useCallback((fetchFn, onSuccess, onError, onLoading) => __awaiter(void 0, void 0, void 0, function* () {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoadingState(true, onLoading);
setError(null);
setRequestCompleted(false);
try {
const data = yield fetchFn(abortController.signal);
if (abortController.signal.aborted) {
return;
}
setResponse(data);
setRequestCompleted(true);
if (onSuccess) {
onSuccess(data);
}
}
catch (err) {
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);
}
}
}), []);
/**
* Executes multiple API requests in parallel and manages loading, error, and success states.
*
* @param fetchFn - Function that performs the fetch and returns a Promise of unknown[]
* @param onSuccess - Optional callback for successful response
* @param onError - Optional callback for error response
* @param onLoading - Optional callback for loading state
* @returns Promise<void>
*/
const executeMultipleRequest = useCallback((fetchFn, onSuccess, onError, onLoading) => __awaiter(void 0, void 0, void 0, function* () {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoadingState(true, onLoading);
setError(null);
setRequestCompleted(false);
try {
const data = yield fetchFn(abortController.signal);
if (abortController.signal.aborted) {
return;
}
setResponse(data);
setRequestCompleted(true);
if (onSuccess) {
onSuccess(data);
}
}
catch (err) {
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);
}
}
}), []);
/**
* Cleans up any ongoing requests when the component unmounts.
*/
useEffect(() => () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
/**
* Performs a fetch request with the specified HTTP method and parameters.
*
* @param method HTTP method (GET, POST, PUT, DELETE, PATCH)
* @param params Optional request parameters
* @param abortSignal Optional abort signal for cancellation
* @returns Promise<T>
*/
const requestWithMethod = useCallback((method, params, abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const url = `${baseUrl}${(_a = params === null || params === void 0 ? void 0 : params.endpoint) !== null && _a !== void 0 ? _a : endpoint}`;
const fetchOptions = {
method,
headers,
signal: abortSignal,
};
if ((params === null || params === void 0 ? void 0 : params.data) &&
(method === 'POST' || method === 'PUT' || method === 'PATCH')) {
fetchOptions.body = JSON.stringify(params.data);
}
const res = yield withTimeout(fetch(url, fetchOptions), 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 (_b) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const json = yield res.json();
return json;
}), [baseUrl, endpoint, headers, timeout]);
/**
* Fetches data from the API using a GET request.
*
* @param params - An object containing:
* - endpoint: (optional) API endpoint to override the default
* - onSuccess: (optional) Callback invoked with response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const fetchData = useCallback((params) => executeRequest((abortSignal) => requestWithMethod('GET', params, abortSignal), params === null || params === void 0 ? void 0 : params.onSuccess, params === null || params === void 0 ? void 0 : params.onError, params === null || params === void 0 ? void 0 : params.onLoading), [executeRequest, requestWithMethod]);
/**
* Sends data to the API using a POST request.
*
* @param params - An object containing:
* - endpoint: (optional) API endpoint to override the default
* - data: (optional) The payload to send in the request body
* - onSuccess: (optional) Callback invoked with response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const postData = useCallback((params) => executeRequest((abortSignal) => requestWithMethod('POST', params, abortSignal), params.onSuccess, params.onError, params.onLoading), [executeRequest, requestWithMethod]);
/**
* Updates data on the API using a PUT request.
*
* @param params - An object containing:
* - endpoint: (optional) API endpoint to override the default
* - data: (optional) The payload to send in the request body
* - onSuccess: (optional) Callback invoked with response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const putData = useCallback((params) => executeRequest((abortSignal) => requestWithMethod('PUT', params, abortSignal), params.onSuccess, params.onError, params.onLoading), [executeRequest, requestWithMethod]);
/**
* Deletes data from the API using a DELETE request.
*
* @param params - An object containing:
* - endpoint: (optional) API endpoint to override the default
* - onSuccess: (optional) Callback invoked with response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const deleteData = useCallback((params) => executeRequest((abortSignal) => requestWithMethod('DELETE', params, abortSignal), params === null || params === void 0 ? void 0 : params.onSuccess, params === null || params === void 0 ? void 0 : params.onError, params === null || params === void 0 ? void 0 : params.onLoading), [executeRequest, requestWithMethod]);
/**
* Partially updates data on the API using a PATCH request.
*
* @param params - An object containing:
* - endpoint: (optional) API endpoint to override the default
* - data: (optional) The payload to send in the request body
* - onSuccess: (optional) Callback invoked with response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const patchData = useCallback((params) => executeRequest((abortSignal) => requestWithMethod('PATCH', params, abortSignal), params.onSuccess, params.onError, params.onLoading), [executeRequest, requestWithMethod]);
/**
* Fetches data from multiple endpoints in parallel using GET requests.
*
* @param params - An object containing:
* - endpoints: Array of endpoint strings to fetch
* - onSuccess: (optional) Callback invoked with array of response data on success
* - onError: (optional) Callback invoked with error object on failure
* - onLoading: (optional) Callback invoked with loading state changes
* @returns void
*/
const fetchMultipleData = useCallback((params) => executeMultipleRequest((abortSignal) => __awaiter(void 0, void 0, void 0, function* () {
const promises = params.endpoints.map((ep) => __awaiter(void 0, void 0, void 0, function* () {
const url = `${baseUrl}${ep}`;
const res = yield withTimeout(fetch(url, { method: 'GET', headers, signal: abortSignal }), 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);
}), params.onSuccess, params.onError, params.onLoading), [baseUrl, headers, executeMultipleRequest, timeout]);
/**
* Creates a chainable API for performing multiple requests in sequence.
*
* @returns ChainableRequest<T> - An object with methods for chaining requests (fetch, post, put, delete, patch) and handling callbacks (then, catch, finally, execute).
*
* @example
* chain()
* .fetch({ endpoint: '/users/1' })
* .put({ endpoint: '/users/1', data: { name: 'New Name' } })
* .then(data => console.log('Success:', data))
* .catch(error => console.error('Error:', error))
* .finally(() => console.log('Done'))
* .execute();
*/
const chain = useCallback(() => {
const operations = [];
let finalSuccessCallback;
let finalErrorCallback;
let finallyCallback;
const chainRequestWithMethod = (method, params) => executeRequest((abortSignal) => requestWithMethod(method, params, abortSignal), params === null || params === void 0 ? void 0 : params.onSuccess, params === null || params === void 0 ? void 0 : params.onError, params === null || params === void 0 ? void 0 : params.onLoading);
const chainableRequest = {
fetch: (params) => {
operations.push({
type: 'GET',
endpoint: params === null || params === void 0 ? void 0 : params.endpoint,
data: params === null || params === void 0 ? void 0 : params.data,
onSuccess: params === null || params === void 0 ? void 0 : params.onSuccess,
onError: params === null || params === void 0 ? void 0 : params.onError,
onLoading: params === null || params === void 0 ? void 0 : params.onLoading,
});
return chainableRequest;
},
post: (params) => {
operations.push({
type: 'POST',
endpoint: params.endpoint,
data: params.data,
onSuccess: params.onSuccess,
onError: params.onError,
onLoading: params.onLoading,
});
return chainableRequest;
},
put: (params) => {
operations.push({
type: 'PUT',
endpoint: params.endpoint,
data: params.data,
onSuccess: params.onSuccess,
onError: params.onError,
onLoading: params.onLoading,
});
return chainableRequest;
},
delete: (params) => {
operations.push({
type: 'DELETE',
endpoint: params === null || params === void 0 ? void 0 : params.endpoint,
data: params === null || params === void 0 ? void 0 : params.data,
onSuccess: params === null || params === void 0 ? void 0 : params.onSuccess,
onError: params === null || params === void 0 ? void 0 : params.onError,
onLoading: params === null || params === void 0 ? void 0 : params.onLoading,
});
return chainableRequest;
},
patch: (params) => {
operations.push({
type: 'PATCH',
endpoint: params.endpoint,
data: params.data,
onSuccess: params.onSuccess,
onError: params.onError,
onLoading: params.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;
const chainPromise = operations.reduce((promise, operation) => promise.then(() => chainRequestWithMethod(operation.type, operation).then((result) => {
if (result !== undefined) {
lastResult = result;
if (operation.onSuccess && lastResult !== null) {
operation.onSuccess(lastResult);
}
}
})), Promise.resolve());
yield chainPromise
.then(() => {
if (finalSuccessCallback && lastResult !== null) {
finalSuccessCallback(lastResult);
}
})
.catch((err) => {
if (finalErrorCallback) {
finalErrorCallback(err);
}
throw err;
})
.finally(() => {
if (finallyCallback) {
finallyCallback();
}
});
}),
};
return chainableRequest;
}, [executeRequest, requestWithMethod]);
// Helper for timeout logic
/**
* Helper for timeout logic. Use outside the hook to avoid useCallback dependency issues.
*/
function withTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Request timeout')), ms);
promise
.then((val) => {
clearTimeout(timer);
resolve(val);
})
.catch((err) => {
clearTimeout(timer);
reject(err);
});
});
}
/**
* Hook return value: provides API response, loading/error states, and request methods.
*
* @property response The API response data
* @property loading Loading state
* @property error Error state
* @property requestCompleted Whether the request has completed
* @property fetchData GET request method
* @property postData POST request method
* @property putData PUT request method
* @property deleteData DELETE request method
* @property patchData PATCH request method
* @property fetchMultipleData Multiple GET requests method
* @property chain Chainable API for sequential requests
*/
return {
response,
loading,
error,
requestCompleted,
fetchData,
postData,
putData,
deleteData,
patchData,
fetchMultipleData,
chain,
};
};
export default useFetchWithCallbacks;