UNPKG

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
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;