@oxyhq/services
Version:
130 lines (113 loc) • 3.68 kB
text/typescript
import { useState, useCallback } from 'react';
import { toast } from '../../lib/sonner';
interface UseAsyncActionOptions<T> {
/** Function to execute */
action: () => Promise<T>;
/** Success message to display */
successMessage?: string;
/** Error message to display (or function to get message from error) */
errorMessage?: string | ((error: any) => string);
/** Callback on success */
onSuccess?: (result: T) => void;
/** Callback on error */
onError?: (error: any) => void;
/** Show loading toast */
showLoadingToast?: boolean;
/** Loading message */
loadingMessage?: string;
}
interface UseAsyncActionReturn<T> {
/** Execute the action */
execute: () => Promise<T | undefined>;
/** Whether the action is currently executing */
isLoading: boolean;
/** The last error that occurred */
error: Error | null;
/** Reset the error state */
resetError: () => void;
}
/**
* Hook for handling async actions with loading state, error handling, and toast notifications.
* Reduces boilerplate for common patterns like try-catch with toast feedback.
*
* @example
* const { execute, isLoading } = useAsyncAction({
* action: () => api.saveSettings(settings),
* successMessage: 'Settings saved!',
* errorMessage: 'Failed to save settings',
* });
*
* <Button onPress={execute} disabled={isLoading}>Save</Button>
*/
export function useAsyncAction<T = void>(
options: UseAsyncActionOptions<T>
): UseAsyncActionReturn<T> {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async (): Promise<T | undefined> => {
const {
action,
successMessage,
errorMessage,
onSuccess,
onError,
showLoadingToast,
loadingMessage,
} = options;
setIsLoading(true);
setError(null);
if (showLoadingToast && loadingMessage) {
toast.loading(loadingMessage);
}
try {
const result = await action();
if (successMessage) {
toast.success(successMessage);
}
onSuccess?.(result);
return result;
} catch (err: any) {
const message = typeof errorMessage === 'function'
? errorMessage(err)
: errorMessage || err?.message || 'An error occurred';
toast.error(message);
setError(err instanceof Error ? err : new Error(message));
onError?.(err);
return undefined;
} finally {
setIsLoading(false);
}
}, [options]);
const resetError = useCallback(() => {
setError(null);
}, []);
return { execute, isLoading, error, resetError };
}
/**
* Simplified version that just executes an async action with toast feedback.
* Useful for one-off actions.
*/
export async function executeWithToast<T>(
action: () => Promise<T>,
options?: {
successMessage?: string;
errorMessage?: string;
loadingMessage?: string;
}
): Promise<T | undefined> {
const { successMessage, errorMessage, loadingMessage } = options || {};
if (loadingMessage) {
toast.loading(loadingMessage);
}
try {
const result = await action();
if (successMessage) {
toast.success(successMessage);
}
return result;
} catch (err: any) {
toast.error(errorMessage || err?.message || 'An error occurred');
return undefined;
}
}
export default useAsyncAction;