/**
* useAsync Hook
* Async function wrapper with loading, error, and data state management
*
* Features:
* - Wraps async functions with automatic loading/error/data handling
* - Generic typing for async function return types
* - Manual execution with execute() method
* - Reset functionality to clear data and errors
* - Automatic cleanup to prevent memory leaks
* - Dependency tracking for re-execution
*
* @example
* const fetchUser = async (id: string) => {
* const res = await fetch(`/api/users/${id}`)
* return res.json()
* }
*
* const { data, loading, error, execute } = useAsync(fetchUser, ['userId'])
*
*
* {loading &&
}
* {error &&
{error.message}}
* {data &&
}
*
*
*
* @example
* // With automatic execution on mount
* const { data: posts, loading, error } = useAsync(
* async () => {
* const res = await fetch('/api/posts')
* return res.json()
* },
* [],
* { immediate: true }
* )
*
* @example
* // With error handling and retry
* const { data, loading, error, execute } = useAsync(
* async () => {
* try {
* const res = await fetch('/api/data')
* if (!res.ok) throw new Error(`HTTP ${res.status}`)
* return res.json()
* } catch (err) {
* throw err
* }
* }
* )
*
* const handleRetry = async () => {
* await execute()
* }
*/
import { useState, useCallback, useEffect, useRef } from 'react'
export interface UseAsyncOptions {
/** Execute immediately on mount (default: false) */
immediate?: boolean
/** Reset error on retry (default: true) */
resetErrorOnRetry?: boolean
/** Reset data on retry (default: false) */
resetDataOnRetry?: boolean
}
export interface UseAsyncReturn {
/** Result data from async function */
data: T | undefined
/** Loading state */
loading: boolean
/** Error object if function threw */
error: Error | undefined
/** Execute the async function */
execute: (...args: any[]) => Promise
/** Reset data, error, and loading state */
reset: () => void
}
/**
* Hook for managing async function execution with state
* @template T - The return type of the async function
* @param asyncFunction - Async function to execute
* @param deps - Dependency array for re-execution (default: [])
* @param options - Configuration options
* @returns Object containing data, loading, error, and execute method
*/
export function useAsync(
asyncFunction: (...args: any[]) => Promise,
deps: any[] = [],
options: UseAsyncOptions = {}
): UseAsyncReturn {
const { immediate = false, resetErrorOnRetry = true, resetDataOnRetry = false } = options
const [data, setData] = useState(undefined)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(undefined)
const isMountedRef = useRef(true)
// Cleanup on unmount
useEffect(() => {
return () => {
isMountedRef.current = false
}
}, [])
const execute = useCallback(
async (...args: any[]): Promise => {
// Reset states if requested
if (resetErrorOnRetry) setError(undefined)
if (resetDataOnRetry) setData(undefined)
setLoading(true)
try {
const result = await asyncFunction(...args)
// Only update state if component is still mounted
if (isMountedRef.current) {
setData(result)
setError(undefined)
return result
}
} catch (err) {
// Only update state if component is still mounted
if (isMountedRef.current) {
const error = err instanceof Error ? err : new Error(String(err))
setError(error)
setData(undefined)
}
} finally {
if (isMountedRef.current) {
setLoading(false)
}
}
return undefined
},
[asyncFunction, resetErrorOnRetry, resetDataOnRetry]
)
const reset = useCallback(() => {
setData(undefined)
setError(undefined)
setLoading(false)
}, [])
// Execute immediately if requested
useEffect(() => {
if (immediate) {
execute()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
return {
data,
loading,
error,
execute,
reset,
}
}