Skip to content

empty916/great-async

Repository files navigation

great-async

πŸš€ A powerful async operation library that makes async operations effortless, with built-in caching, SWR, debouncing, and more.

npm version License: MIT

Why great-async?

  • 🎯 Framework Agnostic - Works with any JavaScript environment
  • ⚑ SWR Pattern - Show cached data instantly, update in background
  • πŸ”„ Smart Caching - TTL and LRU cache strategies
  • 🚫 Duplicate Prevention - Merge identical concurrent requests
  • πŸ” Auto Retry - Configurable retry logic with custom strategies
  • ⏰ Debouncing - Control when functions execute
  • βš›οΈ React Ready - Built-in hooks with loading states

Installation

npm install great-async

Core API - createAsync

The heart of great-async is createAsync - a framework-agnostic function that enhances any async function with powerful features.

Basic Usage

// Recommended: Use the modern API
import { createAsync } from 'great-async';
import { createAsync } from 'great-async/create-async';

// Legacy: Use the full name (deprecated)
import { createAsyncController } from 'great-async';
import { createAsyncController } from 'great-async/asyncController';

// Enhance any async function
const fetchUserData = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

const enhancedFetch = createAsync(fetchUserData, {
  ttl: 60000, // Cache for 1 minute
  swr: true,  // Enable stale-while-revalidate
});

// Use it like the original function
const userData = await enhancedFetch('123');

Core Features

πŸ”„ Smart Caching

// Define the API function
const fetchData = async (param: string) => {
  const response = await fetch(`/api/data/${param}`);
  return response.json();
};

const cachedAPI = createAsync(fetchData, {
  ttl: 5 * 60 * 1000,     // Cache for 5 minutes
  cacheCapacity: 100,      // LRU cache with max 100 items
});

// First call: hits the API
const data1 = await cachedAPI('param1');

// Second call within 5 minutes: returns cached data
const data2 = await cachedAPI('param1'); // ⚑ Instant!

⚑ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API function
const fetchUserProfile = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}/profile`);
  return response.json();
};

const swrAPI = createAsync(fetchUserProfile, {
  swr: true,
  ttl: 60000,
  onBackgroundUpdate: (freshData, error) => {
    if (freshData) console.log('Data updated in background!');
    if (error) console.error('Background update failed:', error);
  },
});

// First call: normal API request
await swrAPI('user123');

// Subsequent calls: instant cached response + background update
const profile = await swrAPI('user123'); // ⚑ Returns cached data immediately
// Background: fetches fresh data and updates cache

🎯 Take Latest Promise

When multiple identical requests are made, only the latest one's result is used and all pending requests share its result:

// Define the API function
const performSearch = async (query: string) => {
  const response = await fetch(`/api/search?q=${query}`);
  return response.json();
};

const searchAPI = createAsync(performSearch, {
  takeLatest: true,
});

// Make multiple calls in quick succession
const promise1 = searchAPI('react'); // Starts execution
const promise2 = searchAPI('react'); // Starts execution, promise1 result will be discarded
const promise3 = searchAPI('react'); // Starts execution, promise1 & promise2 results will be discarded

// All promises resolve with the result from the final (3rd) call
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
console.log(result1 === result2 && result2 === result3); // true - all use result from promise3

⏰ Debouncing

Control when functions execute with two different scopes:

import { DIMENSIONS } from 'great-async/asyncController';

// Define the API function
const searchAPI = async (query: string) => {
  const response = await fetch(`/api/search?q=${query}`);
  return response.json();
};

// PARAMETERS dimension: Debounce per unique parameters
const parameterDebounce = createAsync(searchAPI, {
  debounceTime: 300,
  debounceDimension: DIMENSIONS.PARAMETERS,
});

// Each unique parameter gets its own debounce timer
parameterDebounce('react');  // Timer 1: Will execute after 300ms
parameterDebounce('vue');    // Timer 2: Will execute after 300ms (different parameter)
parameterDebounce('react');  // Cancels Timer 1, starts new timer for 'react'

// FUNCTION dimension: Debounce ignores parameters
const functionDebounce = createAsync(searchAPI, {
  debounceTime: 300,
  debounceDimension: DIMENSIONS.FUNCTION,
});

// All calls share the same debounce timer regardless of parameters
functionDebounce('react');   // Starts global timer
functionDebounce('vue');     // Cancels previous timer, starts new one
functionDebounce('angular'); // Only this call will execute after 300ms

πŸ” Smart Retry Logic

Handle failures gracefully:

// Define the API function
const fetchData = async (param: string) => {
  const response = await fetch(`/api/data/${param}`);
  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}`);
    (error as any).status = response.status;
    throw error;
  }
  return response.json();
};

const resilientAPI = createAsync(fetchData, {
  retryStrategy: (error, currentRetryCount) => {
    // Retry on server errors, but limit retries for specific errors
    if (error.status >= 500) {
      // For 503 Service Unavailable, only retry first 2 attempts
      if (error.status === 503) {
        return currentRetryCount <= 2;
      }
      // For other server errors, retry all attempts
      return true;
    }
    // Don't retry client errors
    return false;
  },
});

// Automatically retries up to 3 times on 5xx errors
const data = await resilientAPI('important-data');

πŸ“¦ Single Mode

Prevent concurrent executions - all pending requests share the result of the first ongoing request:

// Define the API function
const heavyOperation = async (param: string) => {
  // Simulate a heavy operation
  await new Promise(resolve => setTimeout(resolve, 2000));
  const response = await fetch(`/api/heavy/${param}`);
  return response.json();
};

const singletonAPI = createAsync(heavyOperation, {
  single: true,
});

// Multiple calls during first request execution
const promise1 = singletonAPI('data1'); // Executes immediately
const promise2 = singletonAPI('data2'); // Waits and shares result from first call
const promise3 = singletonAPI('data3'); // Waits and shares result from first call

// All promises resolve with the same result from the first call
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
console.log(result1 === result2 && result2 === result3); // true

Real-World Examples

🌐 Node.js API Client

import { createAsync, DIMENSIONS } from 'great-async/create-async';

class APIClient {
  private cachedGet = createAsync(this.httpGet, {
    ttl: 5 * 60 * 1000,        // 5 minute cache
    cacheCapacity: 200,         // LRU cache
    retryStrategy: (error, currentRetryCount) => {
      return error.status >= 500 && currentRetryCount <= 3;
    },
  });

  private debouncedSearch = createAsync(this.httpGet, {
    debounceTime: 300,
    debounceDimension: DIMENSIONS.PARAMETERS, // Debounce per unique search query
    takeLatest: true,      // Latest search wins, discard previous identical searches
  });

  async getUser(id: string) {
    return this.cachedGet(`/users/${id}`);
  }

  async search(query: string) {
    return this.debouncedSearch(`/search?q=${query}`);
  }

  private async httpGet(url: string) {
    const response = await fetch(`https://api.example.com${url}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }
}

πŸ” Advanced Search System

const createSearchController = (endpoint: string) => {
  return createAsync(
    async (query: string) => {
      const response = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`);
      return response.json();
    },
    {
      // Performance optimizations
      debounceTime: 300,           // Wait for user to stop typing
      takeLatest: true,       // Latest search wins, discard previous identical searches
      
      // Caching strategy
      swr: true,                   // Show cached results instantly
      ttl: 2 * 60 * 1000,         // Cache for 2 minutes
      cacheCapacity: 50,           // Keep last 50 searches
      
      // Reliability
      retryCount: 2,
      retryStrategy: (error) => error.status >= 500,
      
      // Callbacks
      onBackgroundUpdate: (results, error) => {
        if (error) console.warn('Search cache update failed:', error);
      },
    }
  );
};

const searchProducts = createSearchController('/api/products/search');
const searchUsers = createSearchController('/api/users/search');

// Usage
const products = await searchProducts('laptop');    // Fresh search
const moreProducts = await searchProducts('laptop'); // ⚑ Cached + background update

React Integration - useAsync

For React applications, great-async provides useAsync hook that builds on top of createAsync:

Basic React Usage

// Recommended: Use the modern API
import { useAsync } from 'great-async';
import { useAsync } from 'great-async/use-async';

// Legacy: Use the full name (deprecated)
import { useAsyncFunction } from 'great-async';
import { useAsyncFunction } from 'great-async/useAsyncFunction';

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useAsync(
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    { deps: [userId] } // Re-run when userId changes
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>Hello, {data.name}!</div>;
}

Manual Execution with fn

The fn returned by useAsync allows you to manually trigger the async function at any time:

function UserDashboard({ userId }: { userId: string }) {
  // Define the API function
  const getUserData = async (userId: string) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  };

  const { data, loading, error, fn: getUserDataProxy } = useAsync(
    () => getUserData(userId),
    {
      auto: false, // Don't auto-execute on mount
      deps: [userId]
    }
  );

  return (
    <div>
      <button onClick={() => getUserDataProxy()} disabled={loading}>
        {loading ? 'Loading...' : 'Load User Data'}
      </button>
      
      {error && <div>Error: {error.message}</div>}
      {data && (
        <div>
          <h2>{data.name}</h2>
          <p>Email: {data.email}</p>
          <button onClick={() => getUserDataProxy()}>Refresh</button>
        </div>
      )}
    </div>
  );
}

// Advanced: Conditional execution based on user interaction
function SearchResults({ query }: { query: string }) {
  // Define the API function
  const searchAPI = async (query: string) => {
    const response = await fetch(`/api/search?q=${query}`);
    return response.json();
  };

  const { data, loading, fn: searchAPIProxy } = useAsync(
    () => searchAPI(query),
    {
      auto: 'deps-only', // Only search when query changes, not on mount
      deps: [query],
    }
  );

  const handleManualSearch = () => {
    // Force a fresh search regardless of cache
    searchAPIProxy();
  };

  return (
    <div>
      <button onClick={handleManualSearch} disabled={loading}>
        {loading ? 'Searching...' : 'Search Now'}
      </button>
      {data?.map(item => <div key={item.id}>{item.title}</div>)}
    </div>
  );
}

// Form submission example
function CreateUser() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  
  // Define the API function
  const createUserAPI = async (userData: { name: string; email: string }) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData),
    });
    return response.json();
  };
  
  const { data: newUser, loading, error, fn: createUserAPIProxy } = useAsync(
    () => createUserAPI(formData),
    { auto: false } // Only execute when form is submitted
  );

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    createUserAPIProxy(); // Manual execution
  };

  if (newUser) {
    return <div>User created successfully: {newUser.name}</div>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={formData.name}
        onChange={(e) => setFormData(prev => ({...prev, name: e.target.value}))}
        placeholder="Name"
      />
      <input 
        value={formData.email}
        onChange={(e) => setFormData(prev => ({...prev, email: e.target.value}))}
        placeholder="Email"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <div>Error: {error.message}</div>}
    </form>
  );
}

React-Specific Features

πŸ“± Share Loading States

Share loading states across multiple components using the same loadingId:

import { useAsync, useLoadingState } from 'great-async';

// Define the API functions
const fetchUser = async () => {
  const response = await fetch('/api/user');
  return response.json();
};

const fetchUserAvatar = async () => {
  const response = await fetch('/api/user/avatar');
  return response.json();
};

// Multiple components can share the same loading state
function UserProfile() {
  const { data, loading } = useAsync(fetchUser, {
    loadingId: 'user-data', // Shared loading identifier
  });
  
  if (loading) return <div>Profile loading...</div>;
  return <div>User: {data?.name}</div>;
}

function UserAvatar() {
  const { data, loading } = useAsync(fetchUserAvatar, {
    loadingId: 'user-data', // Same loadingId - shares loading state
  });
  
  if (loading) return <div>Avatar loading...</div>;
  return <img src={data?.avatar} alt="User avatar" />;
}

function GlobalLoadingIndicator() {
  const isLoading = useLoadingState('user-data'); // Reacts to shared loading state
  
  return (
    <div className="global-loading">
      {isLoading && <div>πŸ”„ Loading user data...</div>}
    </div>
  );
}

// Usage: All components will show loading state when ANY of them is loading
function App() {
  return (
    <div>
      <GlobalLoadingIndicator />
      <UserProfile />
      <UserAvatar />
    </div>
  );
}

You can also control shared loading states manually:

import { useAsync } from 'great-async/use-async';

// Manual control of shared loading states
function SomeComponent() {
  const handleStartLoading = () => {
    useAsync.showLoading('user-data'); // Show loading for loadingId
  };

  const handleStopLoading = () => {
    useAsync.hideLoading('user-data'); // Hide loading for loadingId
  };
  
  return (
    <div>
      <button onClick={handleStartLoading}>Start Loading</button>
      <button onClick={handleStopLoading}>Stop Loading</button>
    </div>
  );
}

πŸ”„ React SWR Pattern

function Dashboard() {
  // Define the API function
  const fetchCurrentUser = async () => {
    const response = await fetch('/api/user/current');
    return response.json();
  };

  const { data: user, backgroundUpdating } = useAsync(
    fetchCurrentUser,
    {
      swr: true,
      ttl: 2 * 60 * 1000, // 2 minutes
      onBackgroundUpdate: (newData, error) => {
        if (error) toast.error('Failed to sync user data');
      },
    }
  );

  return (
    <div>
      <h1>Welcome, {user?.name}!</h1>
      {backgroundUpdating && <span>πŸ”„ Syncing...</span>}
    </div>
  );
}

πŸ” Search with Debouncing

function SearchBox() {
  const [query, setQuery] = useState('');
  
  // Define the API function
  const searchAPI = async (query: string) => {
    const response = await fetch(`/api/search?q=${query}`);
    return response.json();
  };
  
  const { data: results, loading } = useAsync(
    () => searchAPI(query),
    {
      deps: [query],
      debounceTime: 300,     // Wait for user to stop typing
      takeLatest: true, // Latest search wins, discard previous identical searches
      auto: query.length > 2, // Only search with 3+ characters
    }
  );

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <span>Searching...</span>}
      {results?.map(item => <div key={item.id}>{item.title}</div>)}
    </div>
  );
}

πŸ—‘οΈ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

function UserProfile({ userId }: { userId: string }) {
  // Define the API function
  const fetchUserData = async (userId: string) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  };
  
  const { data, loading, clearCache } = useAsync(
    (id: string = userId) => fetchUserData(id), // Function with parameters and default value
    {
      deps: [userId],
      ttl: 5 * 60 * 1000,
    }
  );

  const handleClearAllCache = () => {
    clearCache(); // Clear all cached data
  };

  const handleClearSpecificCache = () => {
    clearCache(userId); // Clear cache for specific userId
  };

  return (
    <div>
      {data && <div>User: {data.name}</div>}
      <button onClick={handleClearAllCache}>Clear All Cache</button>
      <button onClick={handleClearSpecificCache}>Clear This User's Cache</button>
    </div>
  );
}

Framework-agnostic usage:

// Define the API function
const fetchUserData = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

const userAPI = createAsync(fetchUserData, {
  ttl: 5 * 60 * 1000,
});

// Use the API
const userData = await userAPI('123'); // Cached for 5 minutes

// Clear cache for one specific parameter combination
userAPI.clearCache('123'); // Clear cache only for userId '123'

// Clear all cache
userAPI.clearCache(); // Clear all cached data

// Force fresh data for specific parameter
userAPI.clearCache('123');
const freshData = await userAPI('123'); // Will fetch fresh data

// Note: To clear multiple specific caches, call clearCache multiple times
userAPI.clearCache('123'); // Clear cache for user '123'
userAPI.clearCache('456'); // Clear cache for user '456'
userAPI.clearCache('789'); // Clear cache for user '789'

Important Notes:

  • Single parameter combination: clearCache(...params) only clears cache for one specific parameter combination
  • Batch clearing: To clear multiple specific caches, call clearCache multiple times
  • Parameter matching: Parameters must match exactly (same values, same order) as when the cache was created

Cache management patterns:

// 1. Clear cache on data mutations
const updateUser = async (userId: string, data: any) => {
  await fetch(`/api/users/${userId}`, { method: 'PUT', body: JSON.stringify(data) });
  userAPI.clearCache(userId); // Clear cache for this specific user
};

// 2. Clear cache on logout
const logout = () => {
  userAPI.clearCache(); // Clear all user data cache
  profileAPI.clearCache(); // Clear profile cache
  // ... clear other caches
};

// 3. Clear multiple specific caches
const clearMultipleUsers = (userIds: string[]) => {
  userIds.forEach(userId => {
    userAPI.clearCache(userId); // Clear each user's cache individually
  });
};

// 4. Clear cache for complex parameters
const searchAPI = createAsync(
  async (query: string, filters: { category: string; status: string }) => {
    // ... search logic
  }
);

// Clear cache for specific search
searchAPI.clearCache('react', { category: 'tech', status: 'active' });

// Clear all search cache
searchAPI.clearCache();

// 5. Periodic cache cleanup
setInterval(() => {
  userAPI.clearCache(); // Clear all cache every hour
}, 60 * 60 * 1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

function UserSettings({ userId }: { userId: string }) {
  const [filters, setFilters] = useState({ category: '', status: '' });
  
  // Define the API function
  const fetchUserSettings = async (userId: string, filters: { category: string; status: string }) => {
    const params = new URLSearchParams({ ...filters, userId });
    const response = await fetch(`/api/user/settings?${params}`);
    return response.json();
  };
  
  // Only auto-fetch when filters change, not on initial mount
  const { data: settings, loading, fn: fetchUserSettingsProxy } = useAsync(
    () => fetchUserSettings(userId, filters),
    {
      auto: 'deps-only',  // Don't auto-call on mount, only when deps change
      deps: [userId, filters],
    }
  );

  return (
    <div>
      <button onClick={() => fetchUserSettingsProxy()}>Load Settings</button>
      <FilterControls 
        filters={filters} 
        onChange={setFilters} // Will trigger auto-fetch when changed
      />
      {loading && <div>Loading...</div>}
      {settings && <SettingsPanel data={settings} />}
    </div>
  );
}

API Reference

createAsync(asyncFn, options)

Returns: Enhanced function with additional methods:

  • Enhanced function: Same signature as original function, but with caching, debouncing, etc.
  • clearCache(): Clear all cached data for this function
  • clearCache(...params): Clear cache for one specific parameter combination
const enhancedFn = createAsync(originalFn, options);

// Use like original function
const result = await enhancedFn(param1, param2);

// Clear all cache
enhancedFn.clearCache();

// Clear cache for one specific parameter combination
enhancedFn.clearCache(param1, param2);

Caching Options

Option Type Default Description
ttl number -1 Cache duration in milliseconds
cacheCapacity number -1 Maximum cache size (LRU)
swr boolean false Enable stale-while-revalidate

Performance Options

Option Type Default Description
debounceTime number -1 Debounce delay in milliseconds
debounceDimension DIMENSIONS FUNCTION Debounce scope:
β€’ FUNCTION: Debounce ignores parameters
β€’ PARAMETERS: Debounce per unique parameters
takeLatest boolean false Latest request wins - discard previous identical requests
single boolean false Share result of first ongoing request with all pending requests
singleDimension DIMENSIONS FUNCTION Single mode scope:
β€’ FUNCTION: Single mode ignores parameters
β€’ PARAMETERS: Single mode per unique parameters

Reliability Options

Option Type Default Description
retryCount number 0 ⚠️ Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategy function () => true Custom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCount
const oldWay = createAsync(apiCall, {
  retryCount: 3,
  retryStrategy: (error) => error.status >= 500
});

// βœ… Recommended: Using retryStrategy only (independent control)
const newWay = createAsync(apiCall, {
  retryStrategy: (error, currentRetryCount) => {
    return currentRetryCount <= 3 && error.status >= 500;
  }
});

// βœ… Advanced: Complex retry logic without retryCount
const advancedWay = createAsync(apiCall, {
  retryStrategy: (error, currentRetryCount) => {
    // Network errors: retry first 2 attempts
    if (error.type === 'network') {
      return currentRetryCount <= 2;
    }

    // Rate limiting: retry with exponential backoff
    if (error.status === 429) {
      return currentRetryCount <= 5;
    }

    // Server errors: retry first 3 attempts
    if (error.status >= 500) {
      return currentRetryCount <= 3;
    }

    // Don't retry client errors
    return false;
  }
});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)
const smartRetry = createAsync(apiCall, {
  retryStrategy: (error, currentRetryCount) => {
    // Don't retry client errors (4xx)
    if (error.status >= 400 && error.status < 500) {
      return false;
    }

    // Rate limiting: retry with increasing delays
    if (error.status === 429) {
      return currentRetryCount <= 5;
    }

    // Server errors: retry first 3 attempts
    if (error.status >= 500) {
      return currentRetryCount <= 3;
    }

    // Network errors: retry first 2 attempts only
    if (error.message.includes('network') || error.message.includes('timeout')) {
      return currentRetryCount <= 2;
    }

    return false;
  }
});

// Example 2: Error-type based independent retry
const typeBasedRetry = createAsync(fetchData, {
  retryStrategy: (error, currentRetryCount) => {
    // Critical operations: retry up to 5 times
    if (error.critical) {
      return currentRetryCount <= 5;
    }

    // Regular operations: retry up to 2 times
    return currentRetryCount <= 2;
  }
});

// Example 3: Backward compatible (with retryCount)
const legacyRetry = createAsync(fetchData, {
  retryCount: 3,
  retryStrategy: (error) => {
    // Old style - still works
    return error.status >= 500;
  }
});

// Example 4: No retry configuration (default behavior)
const noRetry = createAsync(fetchData, {
  // No retry parameters - will not retry on errors
});

Callbacks

Option Type Description
beforeRun () => void Called before function execution
onBackgroundUpdate (data, error) => void Called when SWR background update completes
onBackgroundUpdateStart (cachedData) => void Called when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

Option Type Default Description
auto boolean | 'deps-only' true Control auto-execution behavior:
β€’ true: Auto-call on mount and deps change
β€’ false: Manual execution only
β€’ 'deps-only': Auto-call only when deps change
deps Array [] Re-run when dependencies change
loadingId string '' Share loading state across components

Return Values

Property Type Description
data T | null The result data
loading boolean True during initial load
error any Error object if request fails
backgroundUpdating boolean True during SWR background updates
fn Function Manually trigger the async function
clearCache Function Clear cached data:
β€’ clearCache() - Clear all cached data
β€’ clearCache(...params) - Clear cache for one specific parameter combination

Subpath Imports

Starting from version 1.0.7-beta10, you can import individual modules. Multiple import paths are supported for better compatibility:

// Recommended: Use modern API names with kebab-case
import { createAsync } from 'great-async/create-async';
import { useAsync } from 'great-async/use-async';

// Legacy: Use full API names (deprecated)
import { createAsyncController } from 'great-async/asyncController';
import { useAsyncFunction } from 'great-async/useAsyncFunction';

// Alternative: direct dist imports for better bundler compatibility
import { createAsync } from 'great-async/dist/create-async';
import { useAsync } from 'great-async/dist/use-async';
import { createAsyncController } from 'great-async/dist/asyncController';
import { useAsyncFunction } from 'great-async/dist/useAsyncFunction';

// Utility modules (kebab-case)
import { createTakeLatestPromise } from 'great-async/take-latest-promise';
import { shareLoading } from 'great-async/share-loading';

TypeScript Support

Starting from version 1.0.7-beta10, TypeScript module resolution is fully supported for all import methods. Both runtime and TypeScript compilation will work correctly in all modern bundlers including UMI, Webpack, Vite, etc.

Comparison with Similar Libraries

πŸ“Š Feature Comparison

Feature great-async TanStack Query SWR RTK Query Apollo Client
Framework Support βœ… Agnostic βš›οΈ React βš›οΈ React βš›οΈ React βš›οΈ React
Bundle Size 🟒 ~8KB 🟑 ~47KB 🟒 ~2KB 🟑 ~13KB πŸ”΄ ~47KB
Learning Curve 🟒 Low 🟑 Medium 🟒 Low 🟑 Medium πŸ”΄ High
Caching Strategy βœ… TTL + LRU βœ… Time-based βœ… SWR βœ… Normalized βœ… Normalized
SWR Pattern βœ… Built-in βœ… Built-in βœ… Native βœ… Built-in βœ… Built-in
Debouncing βœ… Advanced ❌ External ❌ External ❌ External ❌ External
Single Mode βœ… Built-in ❌ Manual ❌ Manual ❌ Manual ❌ Manual
Take Latest Promise βœ… Built-in ❌ No ❌ No ❌ No ❌ No
Retry Logic βœ… Configurable βœ… Advanced βœ… Basic βœ… Basic βœ… Advanced
Offline Support βœ… Cache-based βœ… Advanced βœ… Basic βœ… Basic βœ… Advanced
DevTools ❌ No βœ… Excellent ❌ No βœ… Redux βœ… Excellent
Mutations βœ… Via Controller βœ… Built-in βœ… Via mutate βœ… Built-in βœ… Built-in
Share Loading βœ… Unique ❌ No ❌ No ❌ No ❌ No
Auto Modes βœ… 3 modes βœ… Manual βœ… Manual βœ… Manual βœ… Manual
Function Enhancement βœ… Transparent ❌ No ❌ No ❌ No ❌ No
Manual Execution βœ… Simple fn() 🟑 refetch() 🟑 mutate() 🟑 Via endpoints 🟑 refetch()

🎯 When to Choose What

Choose great-async when:

  • βœ… You need a framework-agnostic solution
  • βœ… You want transparent function enhancement - enhance functions without changing their API
  • βœ… You need gradual migration without breaking existing code
  • βœ… You want intuitive manual execution with fn() that preserves function signature
  • βœ… You want advanced debouncing with parameter/function dimensions
  • βœ… You need share loading states across components
  • βœ… You prefer small bundle size with comprehensive features
  • βœ… You want built-in single mode to prevent duplicate requests
  • βœ… You need flexible auto-execution modes (true, false, 'deps-only')
  • βœ… You're building Node.js APIs or vanilla JS applications

Choose TanStack Query when:

  • βœ… You need powerful DevTools for debugging
  • βœ… You want advanced mutation features with optimistic updates
  • βœ… You need infinite queries and complex pagination
  • βœ… You're building large-scale React applications
  • βœ… You want extensive plugin ecosystem

Choose SWR when:

  • βœ… You prefer minimal setup and simplicity
  • βœ… You're using Next.js (made by same team)
  • βœ… You want lightweight solution for basic data fetching
  • βœ… You need fast initial page loads

Choose RTK Query when:

  • βœ… You're already using Redux Toolkit
  • βœ… You need centralized state management
  • βœ… You want normalized caching with entity relationships
  • βœ… You prefer Redux ecosystem and patterns

Choose Apollo Client when:

  • βœ… You're using GraphQL exclusively
  • βœ… You need advanced GraphQL features (subscriptions, fragments)
  • βœ… You want powerful caching with normalized data
  • βœ… You're building complex GraphQL applications

πŸ’‘ Code Comparison

Function Enhancement Pattern - Transparent Proxy Design

// great-async - Transparent Function Enhancement
// Original function
async function fetchUserData(userId: string) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!
const enhancedFetchUser = createAsync(fetchUserData, {
  ttl: 5 * 60 * 1000,
  debounceTime: 300,
  retryCount: 3,
  swr: true,
});

// Use exactly like the original function
const userData = await enhancedFetchUser('123'); // βœ… Same API!
const moreData = await enhancedFetchUser('456'); // βœ… With all enhancements!

// Perfect for gradual migration - just replace the function!
// Before: const users = await Promise.all([fetchUserData('1'), fetchUserData('2')])
// After:  const users = await Promise.all([enhancedFetchUser('1'), enhancedFetchUser('2')])

// Works in any context - classes, modules, callbacks
class UserService {
  fetchUser = enhancedFetchUser; // βœ… Drop-in replacement
  
  async getTeam(userIds: string[]) {
    return Promise.all(userIds.map(this.fetchUser)); // βœ… Same usage
  }
}

// Other libraries - Require different usage patterns
// TanStack Query - Must use hooks, different API
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUserData(userId), // ❌ Wrapped in hook
});

// SWR - Must use hooks, different API  
const { data } = useSWR(
  ['user', userId], 
  () => fetchUserData(userId) // ❌ Wrapped in hook
);

// RTK Query - Must define endpoints, different API
const api = createApi({
  endpoints: (builder) => ({
    getUser: builder.query({ // ❌ Completely different API
      query: (userId) => `/users/${userId}`,
    }),
  }),
});

Simple Data Fetching

// Define the API function
const getUserData = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

// great-async - Framework Agnostic
const fetchUser = createAsync(getUserData, {
  ttl: 5 * 60 * 1000,
  swr: true,
});

// React usage with manual control
const { data, loading, error, fn: fetchUserProxy } = useAsync(
  () => fetchUser(userId),
  {
    deps: [userId],
    auto: 'deps-only'
  }
);

// Manual execution - same function signature!
const handleRefresh = () => fetchUserProxy(); // βœ… Simple and intuitive

// TanStack Query - React Only
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => getUserData(userId),
  staleTime: 5 * 60 * 1000,
});

// Manual execution - different API
const handleRefresh = () => refetch(); // ❌ Different function, loses parameters

// SWR - React Only
const { data, isLoading, error, mutate } = useSWR(
  ['user', userId],
  () => getUserData(userId)
);

// Manual execution - complex API
const handleRefresh = () => mutate(); // ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functions
const performSearch = async (query: string) => {
  const response = await fetch(`/api/search?q=${query}`);
  return response.json();
};

const fetchUserProfile = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}/profile`);
  return response.json();
};

// great-async - Unique Features
const searchAPI = createAsync(performSearch, {
  debounceTime: 300,
  debounceDimension: DIMENSIONS.PARAMETERS, // Per-parameter debouncing
  takeLatest: true, // Latest request wins
  swr: true,
  retryStrategy: (error, currentRetryCount) => {
    return error.status >= 500 && currentRetryCount <= 3;
  },
});

// TanStack Query - Requires additional setup
const { data, isLoading } = useQuery({
  queryKey: ['search', query],
  queryFn: () => performSearch(query),
  enabled: !!query,
  retry: 3,
});

// Manual debouncing needed
const debouncedQuery = useDebounce(query, 300);

πŸš€ Migration Examples

From SWR to great-async

// Define the API function
const fetchUserData = async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

// Before (SWR)
const { data, error, isLoading, mutate } = useSWR(
  `/api/users/${userId}`,
  fetcher,
  { refreshInterval: 30000 }
);

// Manual refresh requires revalidation
const handleRefresh = () => mutate(); // ❌ Complex revalidation logic

// After (great-async)
const { data, error, loading, fn: fetchUserDataProxy } = useAsync(
  (id: string = userId) => fetchUserData(id),
  {
    deps: [userId],
    ttl: 30000,
    swr: true,
  }
);

// Manual refresh is simple and intuitive
const handleRefresh = () => fetchUserDataProxy(); // βœ… Direct function call

From TanStack Query to great-async

// Define the API function
const fetchPosts = async (params: { page: number }) => {
  const response = await fetch(`/api/posts?page=${params.page}`);
  return response.json();
};

// Before (TanStack Query)
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['posts', { page }],
  queryFn: ({ queryKey }) => fetchPosts(queryKey[1]),
  staleTime: 5 * 60 * 1000,
});

// Manual refetch loses original parameters
const handleRefresh = () => refetch(); // ❌ No control over parameters

// After (great-async)
const { data, loading, error, fn: fetchPostsProxy } = useAsync(
  (params: { page: number } = { page }) => fetchPosts(params),
  {
    deps: [page],
    ttl: 5 * 60 * 1000,
    swr: true,
  }
);

// Manual execution with full control
const handleRefresh = () => fetchPostsProxy(); // βœ… Same function, same parameters
const handleRefreshWithNewPage = () => fetchPostsProxy({ page: page + 1 }); // βœ… Can modify parameters

πŸ“ˆ Performance Comparison

Library Bundle Size Runtime Performance Memory Usage
great-async 🟒 ~8KB 🟒 Excellent 🟒 Low
TanStack Query 🟑 ~47KB 🟒 Excellent 🟑 Medium
SWR 🟒 ~2KB 🟒 Excellent 🟒 Low
RTK Query 🟑 ~13KB 🟒 Good 🟑 Medium
Apollo Client πŸ”΄ ~47KB 🟑 Good πŸ”΄ High

πŸ† Summary

great-async stands out by offering:

  1. Framework Agnostic: Works everywhere (React, Vue, Node.js, vanilla JS)
  2. Transparent Function Enhancement: Enhance functions without changing their API
  3. Intuitive Manual Execution: fn() preserves original function signature and behavior
  4. Unique Features: Advanced debouncing, share loading states, single mode
  5. Small Bundle: Comprehensive features in a compact package
  6. Simple API: Easy to learn and use
  7. Flexible: Multiple auto-execution modes and caching strategies

While other libraries excel in specific areas (TanStack Query's DevTools, SWR's simplicity, RTK Query's Redux integration), great-async provides the best balance of features, performance, and flexibility for most use cases.

Migration Guide

From other libraries

// From SWR
- import useSWR from 'swr'
+ import { useAsync } from 'great-async'

- const { data, error } = useSWR('/api/user', fetcher)
+ const { data, error } = useAsync(fetchUser, { swr: true })

// From React Query
- import { useQuery } from 'react-query'
+ import { useAsync } from 'great-async'

- const { data, isLoading } = useQuery('user', fetchUser)
+ const { data, loading } = useAsync(fetchUser, { ttl: 300000 })

Best Practices

βœ… Do's

  • Start with createAsync for framework-agnostic code
  • Use swr: true for data that doesn't change often
  • Set appropriate ttl values based on data freshness needs
  • Use debounceTime for user input-triggered requests
  • Use retryStrategy instead of deprecated retryCount for flexible retry control
  • Use deps array in React to control when requests re-run
  • Use auto: 'deps-only' for conditional data loading (e.g., search, filters)
  • Prefer auto: false for expensive operations that should be manually triggered

❌ Don'ts

  • Don't set very short TTL values (< 1 second) without good reason
  • Don't use SWR for real-time data that must be always fresh
  • Don't forget to handle errors in production
  • Don't set cacheCapacity too high in memory-constrained environments
  • Don't use deprecated retryCount - use retryStrategy instead for better control
  • Don't combine single: true with debounceTime - these features conflict with each other

⚠️ Feature Conflicts

Single Mode vs Debouncing

Avoid using single: true together with debounceTime as they have conflicting behaviors:

  • Debounce: Delays execution until user stops making calls
  • Single: Prevents duplicate executions by sharing ongoing requests
// ❌ BAD: Conflicting configuration
const conflictedAPI = createAsync(searchFn, {
  debounceTime: 300,  // Delays execution
  single: true,       // Shares ongoing requests - CONFLICTS!
});

// βœ… GOOD: Use debounce for user input
const searchAPI = createAsync(searchFn, {
  debounceTime: 300,
  takeLatest: true, // Latest request wins
});

// βœ… GOOD: Use single for expensive operations
const heavyAPI = createAsync(heavyFn, {
  single: true,
  ttl: 60000, // Cache results
});

License

MIT Β© great-async

About

make async great again,hhh

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published