Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

great-async

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

npm versionLicense: 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 APIimport{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 functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constenhancedFetch=createAsync(fetchUserData,{ttl: 60000,// Cache for 1 minuteswr: true,// Enable stale-while-revalidate});// Use it like the original functionconstuserData=awaitenhancedFetch('123');

Core Features

🔄 Smart Caching

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);returnresponse.json();};constcachedAPI=createAsync(fetchData,{ttl: 5*60*1000,// Cache for 5 minutescacheCapacity: 100,// LRU cache with max 100 items});// First call: hits the APIconstdata1=awaitcachedAPI('param1');// Second call within 5 minutes: returns cached dataconstdata2=awaitcachedAPI('param1');// ⚡ Instant!

⚡ SWR (Stale-While-Revalidate)

Perfect for improving perceived performance:

// Define the API functionconstfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};constswrAPI=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 requestawaitswrAPI('user123');// Subsequent calls: instant cached response + background updateconstprofile=awaitswrAPI('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 functionconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constsearchAPI=createAsync(performSearch,{takeLatest: true,});// Make multiple calls in quick successionconstpromise1=searchAPI('react');// Starts executionconstpromise2=searchAPI('react');// Starts execution, promise1 result will be discardedconstpromise3=searchAPI('react');// Starts execution, promise1 & promise2 results will be discarded// All promises resolve with the result from the final (3rd) callconst[result1,result2,result3]=awaitPromise.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 functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};// PARAMETERS dimension: Debounce per unique parametersconstparameterDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,});// Each unique parameter gets its own debounce timerparameterDebounce('react');// Timer 1: Will execute after 300msparameterDebounce('vue');// Timer 2: Will execute after 300ms (different parameter)parameterDebounce('react');// Cancels Timer 1, starts new timer for 'react'// FUNCTION dimension: Debounce ignores parametersconstfunctionDebounce=createAsync(searchAPI,{debounceTime: 300,debounceDimension: DIMENSIONS.FUNCTION,});// All calls share the same debounce timer regardless of parametersfunctionDebounce('react');// Starts global timerfunctionDebounce('vue');// Cancels previous timer, starts new onefunctionDebounce('angular');// Only this call will execute after 300ms

🔁 Smart Retry Logic

Handle failures gracefully:

// Define the API functionconstfetchData=async(param: string)=>{constresponse=awaitfetch(`/api/data/${param}`);if(!response.ok){consterror=newError(`HTTP ${response.status}`);(errorasany).status=response.status;throwerror;}returnresponse.json();};constresilientAPI=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Retry on server errors, but limit retries for specific errorsif(error.status>=500){// For 503 Service Unavailable, only retry first 2 attemptsif(error.status===503){returncurrentRetryCount<=2;}// For other server errors, retry all attemptsreturntrue;}// Don't retry client errorsreturnfalse;},});// Automatically retries up to 3 times on 5xx errorsconstdata=awaitresilientAPI('important-data');

📦 Single Mode

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

// Define the API functionconstheavyOperation=async(param: string)=>{// Simulate a heavy operationawaitnewPromise(resolve=>setTimeout(resolve,2000));constresponse=awaitfetch(`/api/heavy/${param}`);returnresponse.json();};constsingletonAPI=createAsync(heavyOperation,{single: true,});// Multiple calls during first request executionconstpromise1=singletonAPI('data1');// Executes immediatelyconstpromise2=singletonAPI('data2');// Waits and shares result from first callconstpromise3=singletonAPI('data3');// Waits and shares result from first call// All promises resolve with the same result from the first callconst[result1,result2,result3]=awaitPromise.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';classAPIClient{privatecachedGet=createAsync(this.httpGet,{ttl: 5*60*1000,// 5 minute cachecacheCapacity: 200,// LRU cacheretryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});privatedebouncedSearch=createAsync(this.httpGet,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Debounce per unique search querytakeLatest: true,// Latest search wins, discard previous identical searches});asyncgetUser(id: string){returnthis.cachedGet(`/users/${id}`);}asyncsearch(query: string){returnthis.debouncedSearch(`/search?q=${query}`);}privateasynchttpGet(url: string){constresponse=awaitfetch(`https://api.example.com${url}`);if(!response.ok)thrownewError(`HTTP ${response.status}`);returnresponse.json();}}

🔍 Advanced Search System

constcreateSearchController=(endpoint: string)=>{returncreateAsync(async(query: string)=>{constresponse=awaitfetch(`${endpoint}?q=${encodeURIComponent(query)}`);returnresponse.json();},{// Performance optimizationsdebounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searches// Caching strategyswr: true,// Show cached results instantlyttl: 2*60*1000,// Cache for 2 minutescacheCapacity: 50,// Keep last 50 searches// ReliabilityretryCount: 2,retryStrategy: (error)=>error.status>=500,// CallbacksonBackgroundUpdate: (results,error)=>{if(error)console.warn('Search cache update failed:',error);},});};constsearchProducts=createSearchController('/api/products/search');constsearchUsers=createSearchController('/api/users/search');// Usageconstproducts=awaitsearchProducts('laptop');// Fresh searchconstmoreProducts=awaitsearchProducts('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 APIimport{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';functionUserProfile({ 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:

functionUserDashboard({ userId }: {userId: string}){// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, error,fn: getUserDataProxy}=useAsync(()=>getUserData(userId),{auto: false,// Don't auto-execute on mountdeps: [userId]});return(<div><buttononClick={()=>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><buttononClick={()=>getUserDataProxy()}>Refresh</button></div>)}</div>);}// Advanced: Conditional execution based on user interactionfunctionSearchResults({ query }: {query: string}){// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{ data, loading,fn: searchAPIProxy}=useAsync(()=>searchAPI(query),{auto: 'deps-only',// Only search when query changes, not on mountdeps: [query],});consthandleManualSearch=()=>{// Force a fresh search regardless of cachesearchAPIProxy();};return(<div><buttononClick={handleManualSearch}disabled={loading}>{loading ? 'Searching...' : 'Search Now'}</button>{data?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}// Form submission examplefunctionCreateUser(){const[formData,setFormData]=useState({name: '',email: ''});// Define the API functionconstcreateUserAPI=async(userData: {name: string;email: string})=>{constresponse=awaitfetch('/api/users',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(userData),});returnresponse.json();};const{data: newUser, loading, error,fn: createUserAPIProxy}=useAsync(()=>createUserAPI(formData),{auto: false}// Only execute when form is submitted);consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();createUserAPIProxy();// Manual execution};if(newUser){return<div>User created successfully: {newUser.name}</div>;}return(<formonSubmit={handleSubmit}><inputvalue={formData.name}onChange={(e)=>setFormData(prev=>({...prev,name: e.target.value}))}placeholder="Name"/><inputvalue={formData.email}onChange={(e)=>setFormData(prev=>({...prev,email: e.target.value}))}placeholder="Email"/><buttontype="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 functionsconstfetchUser=async()=>{constresponse=awaitfetch('/api/user');returnresponse.json();};constfetchUserAvatar=async()=>{constresponse=awaitfetch('/api/user/avatar');returnresponse.json();};// Multiple components can share the same loading statefunctionUserProfile(){const{ data, loading }=useAsync(fetchUser,{loadingId: 'user-data',// Shared loading identifier});if(loading)return<div>Profile loading...</div>;return<div>User: {data?.name}</div>;}functionUserAvatar(){const{ data, loading }=useAsync(fetchUserAvatar,{loadingId: 'user-data',// Same loadingId - shares loading state});if(loading)return<div>Avatar loading...</div>;return<imgsrc={data?.avatar}alt="User avatar"/>;}functionGlobalLoadingIndicator(){constisLoading=useLoadingState('user-data');// Reacts to shared loading statereturn(<divclassName="global-loading">{isLoading&&<div>🔄 Loading user data...</div>}</div>);}// Usage: All components will show loading state when ANY of them is loadingfunctionApp(){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 statesfunctionSomeComponent(){consthandleStartLoading=()=>{useAsync.showLoading('user-data');// Show loading for loadingId};consthandleStopLoading=()=>{useAsync.hideLoading('user-data');// Hide loading for loadingId};return(<div><buttononClick={handleStartLoading}>Start Loading</button><buttononClick={handleStopLoading}>Stop Loading</button></div>);}

🔄 React SWR Pattern

functionDashboard(){// Define the API functionconstfetchCurrentUser=async()=>{constresponse=awaitfetch('/api/user/current');returnresponse.json();};const{data: user, backgroundUpdating }=useAsync(fetchCurrentUser,{id: 'currentUser',// Required: cache survives remounts, no loading flashswr: true,ttl: 2*60*1000,// 2 minutesonBackgroundUpdate: (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

functionSearchBox(){const[query,setQuery]=useState('');// Define the API functionconstsearchAPI=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};const{data: results, loading }=useAsync(()=>searchAPI(query),{deps: [query],debounceTime: 300,// Wait for user to stop typingtakeLatest: true,// Latest search wins, discard previous identical searchesauto: query.length>2,// Only search with 3+ characters});return(<div><inputvalue={query}onChange={(e)=>setQuery(e.target.value)}placeholder="Search..."/>{loading&&<span>Searching...</span>}{results?.map(item=><divkey={item.id}>{item.title}</div>)}</div>);}

🗑️ Cache Management with clearCache

The clearCache function allows you to manually control cached data:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};const{ data, loading, clearCache }=useAsync((id: string=userId)=>fetchUserData(id),// Function with parameters and default value{deps: [userId],ttl: 5*60*1000,});consthandleClearAllCache=()=>{clearCache();// Clear all cached data};consthandleClearSpecificCache=()=>{clearCache(userId);// Clear cache for specific userId};return(<div>{data&&<div>User: {data.name}</div>}<buttononClick={handleClearAllCache}>Clear All Cache</button><buttononClick={handleClearSpecificCache}>Clear This User's Cache</button></div>);}

Framework-agnostic usage:

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};constuserAPI=createAsync(fetchUserData,{ttl: 5*60*1000,});// Use the APIconstuserData=awaituserAPI('123');// Cached for 5 minutes// Clear cache for one specific parameter combinationuserAPI.clearCache('123');// Clear cache only for userId '123'// Clear all cacheuserAPI.clearCache();// Clear all cached data// Force fresh data for specific parameteruserAPI.clearCache('123');constfreshData=awaituserAPI('123');// Will fetch fresh data// Note: To clear multiple specific caches, call clearCache multiple timesuserAPI.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 mutationsconstupdateUser=async(userId: string,data: any)=>{awaitfetch(`/api/users/${userId}`,{method: 'PUT',body: JSON.stringify(data)});userAPI.clearCache(userId);// Clear cache for this specific user};// 2. Clear cache on logoutconstlogout=()=>{userAPI.clearCache();// Clear all user data cacheprofileAPI.clearCache();// Clear profile cache// ... clear other caches};// 3. Clear multiple specific cachesconstclearMultipleUsers=(userIds: string[])=>{userIds.forEach(userId=>{userAPI.clearCache(userId);// Clear each user's cache individually});};// 4. Clear cache for complex parametersconstsearchAPI=createAsync(async(query: string,filters: {category: string;status: string})=>{// ... search logic});// Clear cache for specific searchsearchAPI.clearCache('react',{category: 'tech',status: 'active'});// Clear all search cachesearchAPI.clearCache();// 5. Periodic cache cleanupsetInterval(()=>{userAPI.clearCache();// Clear all cache every hour},60*60*1000);

🎯 Conditional Auto-Execution

Control when automatic requests are triggered:

functionUserSettings({ userId }: {userId: string}){const[filters,setFilters]=useState({category: '',status: ''});// Define the API functionconstfetchUserSettings=async(userId: string,filters: {category: string;status: string})=>{constparams=newURLSearchParams({ ...filters, userId });constresponse=awaitfetch(`/api/user/settings?${params}`);returnresponse.json();};// Only auto-fetch when filters change, not on initial mountconst{data: settings, loading,fn: fetchUserSettingsProxy}=useAsync(()=>fetchUserSettings(userId,filters),{auto: 'deps-only',// Don't auto-call on mount, only when deps changedeps: [userId,filters],});return(<div><buttononClick={()=>fetchUserSettingsProxy()}>Load Settings</button><FilterControlsfilters={filters}onChange={setFilters}// Will trigger auto-fetch when changed/>{loading&&<div>Loading...</div>}{settings&&<SettingsPaneldata={settings}/>}</div>);}

💾 Persistent Cache Across Mounts

Use the id option to make cache survive component mount/unmount cycles. Without id, the cache is stored in a WeakMap keyed by the function reference and gets garbage-collected when the component unmounts:

functionUserProfile({ userId }: {userId: string}){// Define the API functionconstfetchUserProfile=async(id: string)=>{constresponse=awaitfetch(`/api/users/${id}/profile`);returnresponse.json();};// With `id`, the cache persists even when navigating away and backconst{ data, loading, backgroundUpdating }=useAsync((id: string=userId)=>fetchUserProfile(id),{deps: [userId],id: 'fetchUserProfile',// Stable cache key surviving re-mountsttl: 5*60*1000,swr: true,});if(loading)return<div>Loading...</div>;return(<div><h2>{data?.name}</h2>{backgroundUpdating&&<span>Updating...</span>}</div>);}

How it works: When id is provided, great-async uses a module-level IdCacheManager keyed by this string instead of the default WeakMap<fnProxy> strategy. The cache stays alive as long as the module is loaded — navigate away and back, and SWR still returns the cached data instantly without a loading flash.

⚠️ SWR in React requires id. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Without id, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pair swr: true with an id in React components.

⚠️ Cache key uniqueness. The full cache key is id + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the same id with a no-arg function, they share one cache entry and will overwrite each other's data. To keep caches independent, you must ensure unique full keys. Two ways:

Option 1: Make the function take distinguishing parameters (recommended). The params naturally create unique keys:

// ✅ Different userId → different cache keys under the same idfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync((id: string=userId)=>fetchUser(id),{id: 'fetchUser',swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser:["123"], fetchUser:["456"] — independent!

Option 2: Bake userId into id when the fn is a no-arg closure:

// ✅ Unique id per userId → separate cache entriesfunctionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),// no-arg: closes over userId{id: `fetchUser-${userId}`,swr: true,ttl: 60000,deps: [userId]});}// Keys: fetchUser-123:[], fetchUser-456:[] — independent!
// ❌ BAD: same id + no-arg fn → both instances share key 'fetchUser:[]'functionUserProfile({ userId }: {userId: string}){const{ data }=useAsync(()=>fetchUser(userId),{id: 'fetchUser',swr: true}// overwrites between instances!);}

Manual call mode works the same way — the cache key depends on the args passed to fn():

functionUserProfile({ userId }: {userId: string}){const{ data, fn }=useAsync((id: string)=>fetchUser(id),{id: 'fetchUser',swr: true,auto: false});// cache key = fetchUser:["123"] — derived from fn() args, not depsreturn<buttononClick={()=>fn(userId)}>Load</button>;}

📦 Initial & Fallback Data

Use initialData for the default value before first resolve, and fallbackData to control what happens on error. When fallbackData is omitted, the previously-resolved data is preserved so transient errors don't blank the UI:

functionProductList(){// Define the API functionconstfetchProducts=async()=>{constresponse=awaitfetch('/api/products');if(!response.ok)thrownewError('Failed to fetch');returnresponse.json();// Returns Product[]};const{ data, loading, error }=useAsync(fetchProducts,{initialData: [],// Start with empty array before first resolvefallbackData: [],// Reset to empty array on error (explicit)});// data is always an array — no null check neededreturn(<div>{loading&&<span>Refreshing...</span>}{error&&<div>Error: {error.message}</div>}{data.map(product=>(<divkey={product.id}>{product.name}</div>))}</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
constenhancedFn=createAsync(originalFn,options);// Use like original functionconstresult=awaitenhancedFn(param1,param2);// Clear all cacheenhancedFn.clearCache();// Clear cache for one specific parameter combinationenhancedFn.clearCache(param1,param2);

Caching Options

OptionTypeDefaultDescription
ttlnumber-1Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable
cacheCapacitynumber-1Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable
swrbooleanfalseEnable stale-while-revalidate
idstringStable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount
cacheManagerCacheManager<T>Custom cache manager. Takes precedence over id (with dev warning). The manager is responsible for expiration/eviction — ttl and cacheCapacity are not interpreted by createAsync when this is set

Performance Options

OptionTypeDefaultDescription
debounceTimenumber-1Debounce delay in milliseconds
debounceDimensionDIMENSIONSFUNCTIONDebounce scope:
FUNCTION: Debounce ignores parameters
PARAMETERS: Debounce per unique parameters
takeLatestbooleanfalseLatest request wins - discard previous identical requests
singlebooleanfalseShare result of first ongoing request with all pending requests
singleDimensionDIMENSIONSFUNCTIONSingle mode scope:
FUNCTION: Single mode ignores parameters
PARAMETERS: Single mode per unique parameters

Reliability Options

OptionTypeDefaultDescription
retryCountnumber0⚠️Deprecated - Number of retry attempts (use retryStrategy instead)
retryStrategyfunction() => trueCustom retry logic (error, currentRetryCount) => boolean
Migration from retryCount to retryStrategy
// ❌ Deprecated: Using retryCountconstoldWay=createAsync(apiCall,{retryCount: 3,retryStrategy: (error)=>error.status>=500});// ✅ Recommended: Using retryStrategy only (independent control)constnewWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{returncurrentRetryCount<=3&&error.status>=500;}});// ✅ Advanced: Complex retry logic without retryCountconstadvancedWay=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Network errors: retry first 2 attemptsif(error.type==='network'){returncurrentRetryCount<=2;}// Rate limiting: retry with exponential backoffif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Don't retry client errorsreturnfalse;}});
Advanced Retry Strategy Examples
// Example 1: Independent retry control (no retryCount needed)constsmartRetry=createAsync(apiCall,{retryStrategy: (error,currentRetryCount)=>{// Don't retry client errors (4xx)if(error.status>=400&&error.status<500){returnfalse;}// Rate limiting: retry with increasing delaysif(error.status===429){returncurrentRetryCount<=5;}// Server errors: retry first 3 attemptsif(error.status>=500){returncurrentRetryCount<=3;}// Network errors: retry first 2 attempts onlyif(error.message.includes('network')||error.message.includes('timeout')){returncurrentRetryCount<=2;}returnfalse;}});// Example 2: Error-type based independent retryconsttypeBasedRetry=createAsync(fetchData,{retryStrategy: (error,currentRetryCount)=>{// Critical operations: retry up to 5 timesif(error.critical){returncurrentRetryCount<=5;}// Regular operations: retry up to 2 timesreturncurrentRetryCount<=2;}});// Example 3: Backward compatible (with retryCount)constlegacyRetry=createAsync(fetchData,{retryCount: 3,retryStrategy: (error)=>{// Old style - still worksreturnerror.status>=500;}});// Example 4: No retry configuration (default behavior)constnoRetry=createAsync(fetchData,{// No retry parameters - will not retry on errors});

Callbacks

OptionTypeDescription
beforeRun() => voidCalled before function execution
onBackgroundUpdate(data, error) => voidCalled when SWR background update completes
onBackgroundUpdateStart(cachedData) => voidCalled when SWR background update starts

useAsync(asyncFn, options)

Extends createAsync options with React-specific features:

React-Specific Options

OptionTypeDefaultDescription
autoboolean | 'deps-only'trueControl auto-execution behavior:
true: Auto-call on mount and deps change
false: Manual execution only
'deps-only': Auto-call only when deps change
depsArray[]Re-run when dependencies change
loadingIdstring''Share loading state across components
initialDataTnullValue used for data before the async function first resolves
fallbackDataT | null | undefinedundefinedValue used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI)

Return Values

PropertyTypeDescription
dataT | nullThe result data
loadingbooleanTrue during initial load
erroranyError object if request fails
backgroundUpdatingbooleanTrue during SWR background updates
fnFunctionManually trigger the async function
clearCacheFunctionClear 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-caseimport{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 compatibilityimport{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

Featuregreat-asyncTanStack QuerySWRRTK QueryApollo 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 functionasyncfunctionfetchUserData(userId: string){constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();}// Enhanced function with caching, debouncing, retry - SAME SIGNATURE!constenhancedFetchUser=createAsync(fetchUserData,{ttl: 5*60*1000,debounceTime: 300,retryCount: 3,swr: true,});// Use exactly like the original functionconstuserData=awaitenhancedFetchUser('123');// ✅ Same API!constmoreData=awaitenhancedFetchUser('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, callbacksclassUserService{fetchUser=enhancedFetchUser;// ✅ Drop-in replacementasyncgetTeam(userIds: string[]){returnPromise.all(userIds.map(this.fetchUser));// ✅ Same usage}}// Other libraries - Require different usage patterns// TanStack Query - Must use hooks, different APIconst{ 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 APIconstapi=createApi({endpoints: (builder)=>({getUser: builder.query({// ❌ Completely different APIquery: (userId)=>`/users/${userId}`,}),}),});

Simple Data Fetching

// Define the API functionconstgetUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// great-async - Framework AgnosticconstfetchUser=createAsync(getUserData,{ttl: 5*60*1000,swr: true,});// React usage with manual controlconst{ data, loading, error,fn: fetchUserProxy}=useAsync(()=>fetchUser(userId),{deps: [userId],auto: 'deps-only'});// Manual execution - same function signature!consthandleRefresh=()=>fetchUserProxy();// ✅ Simple and intuitive// TanStack Query - React Onlyconst{ data, isLoading, error, refetch }=useQuery({queryKey: ['user',userId],queryFn: ()=>getUserData(userId),staleTime: 5*60*1000,});// Manual execution - different APIconsthandleRefresh=()=>refetch();// ❌ Different function, loses parameters// SWR - React Onlyconst{ data, isLoading, error, mutate }=useSWR(['user',userId],()=>getUserData(userId));// Manual execution - complex APIconsthandleRefresh=()=>mutate();// ❌ Revalidation only, not re-execution

Advanced Features

// Define the API functionsconstperformSearch=async(query: string)=>{constresponse=awaitfetch(`/api/search?q=${query}`);returnresponse.json();};constfetchUserProfile=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}/profile`);returnresponse.json();};// great-async - Unique FeaturesconstsearchAPI=createAsync(performSearch,{debounceTime: 300,debounceDimension: DIMENSIONS.PARAMETERS,// Per-parameter debouncingtakeLatest: true,// Latest request winsswr: true,retryStrategy: (error,currentRetryCount)=>{returnerror.status>=500&&currentRetryCount<=3;},});// TanStack Query - Requires additional setupconst{ data, isLoading }=useQuery({queryKey: ['search',query],queryFn: ()=>performSearch(query),enabled: !!query,retry: 3,});// Manual debouncing neededconstdebouncedQuery=useDebounce(query,300);

🚀 Migration Examples

From SWR to great-async

// Define the API functionconstfetchUserData=async(userId: string)=>{constresponse=awaitfetch(`/api/users/${userId}`);returnresponse.json();};// Before (SWR)const{ data, error, isLoading, mutate }=useSWR(`/api/users/${userId}`,fetcher,{refreshInterval: 30000});// Manual refresh requires revalidationconsthandleRefresh=()=>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 intuitiveconsthandleRefresh=()=>fetchUserDataProxy();// ✅ Direct function call

From TanStack Query to great-async

// Define the API functionconstfetchPosts=async(params: {page: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}`);returnresponse.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 parametersconsthandleRefresh=()=>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 controlconsthandleRefresh=()=>fetchPostsProxy();// ✅ Same function, same parametersconsthandleRefreshWithNewPage=()=>fetchPostsProxy({page: page+1});// ✅ Can modify parameters

📈 Performance Comparison

LibraryBundle SizeRuntime PerformanceMemory 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-importuseSWRfrom'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 configurationconstconflictedAPI=createAsync(searchFn,{debounceTime: 300,// Delays executionsingle: true,// Shares ongoing requests - CONFLICTS!});// ✅ GOOD: Use debounce for user inputconstsearchAPI=createAsync(searchFn,{debounceTime: 300,takeLatest: true,// Latest request wins});// ✅ GOOD: Use single for expensive operationsconstheavyAPI=createAsync(heavyFn,{single: true,ttl: 60000,// Cache results});

License

MIT © great-async

About

make async great again,hhh

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages