🚀 A powerful async operation library that makes async operations effortless, with built-in caching, SWR, debouncing, and more.
- 🎯 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
npm install great-asyncThe heart of great-async is createAsync - a framework-agnostic function that enhances any async function with powerful features.
// 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');// 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!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 cacheWhen 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 promise3Control 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 300msHandle 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');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);// trueimport{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&¤tRetryCount<=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();}}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 updateFor React applications, great-async provides useAsync hook that builds on top of createAsync:
// 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>;}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>);}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>);}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>);}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>);}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
clearCachemultiple 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);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>);}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 requiresid. The default WeakMap cache is keyed by the fnProxy which gets garbage-collected on unmount. Withoutid, SWR has no cache to serve after a remount and will always show a loading flash on every navigation. Always pairswr: truewith anidin React components.
⚠️ Cache key uniqueness. The full cache key isid + keyGenerator(params). A no-arg function always produces the same params key ("[]"). If two component instances use the sameidwith 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
userIdintoidwhen 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>;}
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>);}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 functionclearCache(...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);| Option | Type | Default | Description |
|---|---|---|---|
ttl | number | -1 | Cache duration in milliseconds. Caching is OFF by default — set ttl or cacheCapacity to enable |
cacheCapacity | number | -1 | Maximum cache size using LRU eviction. Caching is OFF by default — set this or ttl to enable |
swr | boolean | false | Enable stale-while-revalidate |
id | string | — | Stable cache identifier. Uses a module-level store keyed by this id instead of the default WeakMap strategy. Cache survives component mount/unmount |
cacheManager | CacheManager<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 |
| Option | Type | Default | Description |
|---|---|---|---|
debounceTime | number | -1 | Debounce delay in milliseconds |
debounceDimension | DIMENSIONS | FUNCTION | Debounce scope: • FUNCTION: Debounce ignores parameters• PARAMETERS: Debounce per unique parameters |
takeLatest | boolean | false | Latest request wins - discard previous identical requests |
single | boolean | false | Share result of first ongoing request with all pending requests |
singleDimension | DIMENSIONS | FUNCTION | Single mode scope: • FUNCTION: Single mode ignores parameters• PARAMETERS: Single mode per unique parameters |
| Option | Type | Default | Description |
|---|---|---|---|
retryCount | number | 0 | retryStrategy instead) |
retryStrategy | function | () => true | Custom retry logic (error, currentRetryCount) => boolean |
// ❌ 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;}});// 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});| Option | Type | Description |
|---|---|---|
beforeRun | () => void | Called before function execution |
onBackgroundUpdate | (data, error) => void | Called when SWR background update completes |
onBackgroundUpdateStart | (cachedData) => void | Called when SWR background update starts |
Extends createAsync options with React-specific features:
| Option | Type | Default | Description |
|---|---|---|---|
auto | boolean | 'deps-only' | true | Control auto-execution behavior: • true: Auto-call on mount and deps change• false: Manual execution only• 'deps-only': Auto-call only when deps change |
deps | Array | [] | Re-run when dependencies change |
loadingId | string | '' | Share loading state across components |
initialData | T | null | Value used for data before the async function first resolves |
fallbackData | T | null | undefined | undefined | Value used for data when the function rejects. undefined preserves the last-resolved data (transient errors won't blank the UI) |
| Property | Type | Description |
|---|---|---|
data | T | null | The result data |
loading | boolean | True during initial load |
error | any | Error object if request fails |
backgroundUpdating | boolean | True during SWR background updates |
fn | Function | Manually trigger the async function |
clearCache | Function | Clear cached data: • clearCache() - Clear all cached data• clearCache(...params) - Clear cache for one specific parameter combination |
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';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.
| Feature | great-async | TanStack Query | SWR | RTK Query | Apollo Client |
|---|---|---|---|---|---|
| Framework Support | ✅ Agnostic | ⚛️ React | ⚛️ React | ⚛️ React | ⚛️ React |
| Bundle Size | 🟢 ~8KB | 🟡 ~47KB | 🟢 ~2KB | 🟡 ~13KB | 🔴 ~47KB |
| Learning Curve | 🟢 Low | 🟡 Medium | 🟢 Low | 🟡 Medium | 🔴 High |
| Caching Strategy | ✅ TTL + LRU | ✅ Time-based | ✅ SWR | ✅ Normalized | ✅ Normalized |
| SWR Pattern | ✅ Built-in | ✅ Built-in | ✅ Native | ✅ Built-in | ✅ Built-in |
| Debouncing | ✅ Advanced | ❌ External | ❌ External | ❌ External | ❌ External |
| Single Mode | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
| Take Latest Promise | ✅ Built-in | ❌ No | ❌ No | ❌ No | ❌ No |
| Retry Logic | ✅ Configurable | ✅ Advanced | ✅ Basic | ✅ Basic | ✅ Advanced |
| Offline Support | ✅ Cache-based | ✅ Advanced | ✅ Basic | ✅ Basic | ✅ Advanced |
| DevTools | ❌ No | ✅ Excellent | ❌ No | ✅ Redux | ✅ Excellent |
| Mutations | ✅ Via Controller | ✅ Built-in | ✅ Via mutate | ✅ Built-in | ✅ Built-in |
| Share Loading | ✅ Unique | ❌ No | ❌ No | ❌ No | ❌ No |
| Auto Modes | ✅ 3 modes | ✅ Manual | ✅ Manual | ✅ Manual | ✅ Manual |
| Function Enhancement | ✅ Transparent | ❌ No | ❌ No | ❌ No | ❌ No |
| Manual Execution | ✅ Simple fn() | 🟡 refetch() | 🟡 mutate() | 🟡 Via endpoints | 🟡 refetch() |
- ✅ 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
- ✅ 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
- ✅ 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
- ✅ You're already using Redux Toolkit
- ✅ You need centralized state management
- ✅ You want normalized caching with entity relationships
- ✅ You prefer Redux ecosystem and patterns
- ✅ 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
// 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}`,}),}),});// 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// 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&¤tRetryCount<=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);// 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// 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| Library | Bundle Size | Runtime Performance | Memory Usage |
|---|---|---|---|
| great-async | 🟢 ~8KB | 🟢 Excellent | 🟢 Low |
| TanStack Query | 🟡 ~47KB | 🟢 Excellent | 🟡 Medium |
| SWR | 🟢 ~2KB | 🟢 Excellent | 🟢 Low |
| RTK Query | 🟡 ~13KB | 🟢 Good | 🟡 Medium |
| Apollo Client | 🔴 ~47KB | 🟡 Good | 🔴 High |
great-async stands out by offering:
- Framework Agnostic: Works everywhere (React, Vue, Node.js, vanilla JS)
- Transparent Function Enhancement: Enhance functions without changing their API
- Intuitive Manual Execution:
fn()preserves original function signature and behavior - Unique Features: Advanced debouncing, share loading states, single mode
- Small Bundle: Comprehensive features in a compact package
- Simple API: Easy to learn and use
- 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.
// 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})- Start with
createAsyncfor framework-agnostic code - Use
swr: truefor data that doesn't change often - Set appropriate
ttlvalues based on data freshness needs - Use
debounceTimefor user input-triggered requests - Use
retryStrategyinstead of deprecatedretryCountfor flexible retry control - Use
depsarray in React to control when requests re-run - Use
auto: 'deps-only'for conditional data loading (e.g., search, filters) - Prefer
auto: falsefor expensive operations that should be manually triggered
- 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
cacheCapacitytoo high in memory-constrained environments - Don't use deprecated
retryCount- useretryStrategyinstead for better control - Don't combine
single: truewithdebounceTime- these features conflict with each other
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});MIT © great-async