typescriptintermediate

useAsync Hook for Promise Management

Manage async operations with loading, error, and data states using a reusable hook pattern.

typescript
import { useCallback, useEffect, useState } from 'react';

interface AsyncState<T> {
  data: T | null;
  error: Error | null;
  loading: boolean;
}

function useAsync<T>(
  asyncFn: () => Promise<T>,
  deps: unknown[] = [],
  immediate = true,
): AsyncState<T> & { execute: () => Promise<void> } {
  const [state, setState] = useState<AsyncState<T>>({
    data: null,
    error: null,
    loading: immediate,
  });

  const execute = useCallback(async () => {
    setState({ data: null, error: null, loading: true });
    try {
      const data = await asyncFn();
      setState({ data, error: null, loading: false });
    } catch (error) {
      setState({ data: null, error: error as Error, loading: false });
    }
  }, deps); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    if (immediate) execute();
  }, [execute, immediate]);

  return { ...state, execute };
}

// Usage Example
interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const { data, error, loading, execute } = useAsync<User>(
    () => fetch(`/api/users/${userId}`).then(r => r.json()),
    [userId],
  );

  if (loading) return <div className="animate-pulse h-20 bg-gray-800 rounded" />;
  if (error) return (
    <div className="text-red-400">
      Error: {error.message}
      <button onClick={execute} className="ml-2 underline">Retry</button>
    </div>
  );

  return (
    <div className="bg-[#111] border border-white/10 rounded-xl p-4">
      <h2 className="text-white font-bold">{data?.name}</h2>
      <p className="text-gray-400">{data?.email}</p>
    </div>
  );
}

export { useAsync, UserProfile };

Use Cases

  • Data fetching with status tracking
  • Retry-capable API calls
  • Loading state management

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.