@supunlakmal/hooks
Version:
A collection of reusable React hooks
134 lines • 6.64 kB
JavaScript
import { useState, useCallback, useMemo, useRef } from 'react';
/**
* Like `useAsync`, but also provides `AbortSignal` as the first argument to the async function,
* and allows aborting the ongoing operation.
*
* @template T The type of the value resolved by the async function.
* @template E The type of the error rejected by the async function. Defaults to `Error`.
* @template Args The type of the arguments accepted by the async function (excluding the AbortSignal).
* @param asyncFn Function that accepts AbortSignal and other args, and returns a Promise resolving to T.
* @param initialValue Value that will be set on initialisation and on reset.
*/
export const useAsyncAbortable = (// Add export
asyncFn, initialValue) => {
// Ref to hold the AbortController for the current or last execution
const abortControllerRef = useRef(undefined);
// State for loading, error, and resolved value
const [state, setState] = useState({
loading: false,
error: null,
value: initialValue, // Initialize with initialValue
});
// Memoize the internal function that handles abort logic and calls the user's asyncFn
const memoizedFn = useCallback(async (...args) => {
var _a;
// Abort previous async operation if it exists
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
// Create a new controller for the current async call
const ac = new AbortController();
abortControllerRef.current = ac;
try {
// Pass down abort signal and received arguments to the user's function
const result = await asyncFn(ac.signal, ...args);
// If the signal was aborted during the asyncFn execution, throw an error
// Or let asyncFn handle it internally by checking signal.aborted
if (ac.signal.aborted) {
// Optionally throw a specific AbortError or let it proceed if asyncFn handles it.
// For now, let's assume asyncFn might throw or handle it.
// If it resolves despite abort, we proceed, but the controller state is 'aborted'.
}
return result;
}
finally {
// Clear the ref *only* if this specific call is the most recent one
// (i.e., no newer call has overwritten the ref)
if (abortControllerRef.current === ac) {
abortControllerRef.current = undefined;
}
}
}, [asyncFn]); // Dependency: only recreate if the async function itself changes
// Memoized function to execute the async operation
const execute = useCallback(async (...params) => {
setState(() => ({
// Keep previous value during loading for smoother UI, or reset to initialValue
// Let's reset to initialValue for consistency with reset()
value: initialValue,
loading: true,
error: null,
}));
try {
const response = await memoizedFn(...params);
// Check if the operation associated with this state update attempt was aborted *before* setState
// This check is imperfect because the abort might happen between `await` and here.
// Relying on the `finally` block in `memoizedFn` and potentially errors thrown due to abort is more robust.
if (abortControllerRef.current === undefined ||
!abortControllerRef.current.signal.aborted) {
setState({
loading: false,
error: null,
value: response,
});
}
else {
// If it was aborted, keep loading: false but don't update value/error, potentially reset?
// Or rely on the error path if aborting causes memoizedFn to throw.
// Let's assume abort causes an error or is handled gracefully.
// If it resolved *after* being aborted, the state might be ambiguous.
// Current setup: if it resolves successfully even after abort, it sets the state.
// If it throws (e.g., DOMException: AbortError), it goes to the catch block.
}
}
catch (error) {
// Check if the error is due to the abort signal we controlled
if (error instanceof Error && error.name === 'AbortError') {
// If aborted, reset state cleanly or keep error? Let's reset.
setState({
loading: false,
// Setting specific AbortError or null is a design choice.Null is simpler.
error: null, // Or: error as E if E can be AbortError
value: initialValue,
});
}
else {
// Handle other errors
setState({
loading: false,
error: error, // Cast the error to the specified type E
value: initialValue, // Reset value on error
});
}
}
}, [memoizedFn, initialValue]); // Dependencies: the memoized function and initialValue
// Memoized function to reset the state
const reset = useCallback(() => {
var _a;
// Abort any ongoing operation before resetting state
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
abortControllerRef.current = undefined; // Clear the ref explicitly on reset
setState({
loading: false,
error: null,
value: initialValue,
});
}, [initialValue]); // Dependency: initialValue
// Memoized actions object
const actions = useMemo(() => ({
reset,
abort: () => {
var _a;
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
// Optionally update state to reflect abort? e.g., set loading: false
// setState(s => ({...s, loading: false})); // Example
},
execute,
}), [execute, reset]); // Dependencies: the memoized execute and reset functions
// Memoized meta object
const meta = useMemo(() => ({
// Expose the ref object itself. Consumer accesses .current
abortControllerRef: abortControllerRef,
// 'call' is just an alias for execute
call: execute,
}), [execute, abortControllerRef]); // Dependencies: execute and the ref object itself
return [state, actions, meta];
};
//# sourceMappingURL=useAsyncAbortable.js.map