useAsyncFn
Wraps an async function with loading, error and value state; only the newest call updates state, and nothing updates after unmount.
import { useAsyncFn, AsyncFnReturn, AsyncState } from '@salla.sa/twilight-theme-engine/hooks/useAsyncFn';In plain words
When a button starts something slow, such as a request, you want a spinner while it runs and a message if it fails. const [state, run] = useAsyncFn(async () => …) gives you state.loading, state.error and state.value, and calling run() starts the work.
It is the building block of useCoupon, useWishlist and useBlogLike.
Signature
function useAsyncFn<T extends (...args: never[]) => Promise<unknown>>(
fn: T,
deps?: DependencyList, // default []
initialState?: AsyncState<Awaited<ReturnType<T>>> // default { loading: false }
): AsyncFnReturn<T>;
type AsyncFnReturn<T> = [AsyncState<Awaited<ReturnType<T>>>, T];
type AsyncState<T> =
| { loading: boolean; error?: undefined; value?: undefined }
| { loading: true; error?: Error | undefined; value?: T }
| { loading: false; error: Error; value?: undefined }
| { loading: false; error?: undefined; value: T };Try it live
state: {…} 1 keys
import { useAsyncFn } from '@salla.sa/twilight-theme-engine/hooks/useAsyncFn';
export function SlowButton() {
const [state, run] = useAsyncFn(async () => {
await new Promise((resolve) => setTimeout(resolve, 1200));
return 'Job finished';
}, []);
return (
<>
<button onClick={() => void run()} disabled={state.loading}>
{state.loading ? 'Working…' : 'Run'}
</button>
{state.error && <p role="alert">{state.error.message}</p>}
{state.value && <p>{state.value}</p>}
</>
);
}
Example
import { useAsyncFn } from '@salla.sa/twilight-theme-engine/hooks/useAsyncFn';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
export function StockCheck({ productId }: { productId: number }) {
const [state, check] = useAsyncFn(() => product.find(String(productId)), [productId]);
return (
<div>
<button type="button" onClick={() => void check()} disabled={state.loading}>
{state.loading ? 'Checking…' : 'Check stock'}
</button>
{state.error && <p role="alert">{state.error.message}</p>}
{state.value && <p>{state.value.is_out_of_stock ? 'Out of stock' : 'In stock'}</p>}
</div>
);
}
How it behaves
Each call gets an id; a result is applied only if its call is still the newest and the component is still mounted.
Starting a call sets
loading: trueand keeps the previousvalueorerror, so a stale error stays visible while the retry runs unless you hide it onloading.The returned function is
useCallback(fn, deps):depsis the only way new props or state reachfn.For reading data, TanStack Query (the engine's
api/*query options) caches and shares results;useAsyncFnsuits one-off actions.The
@salla.sa/twilight-theme-engine/hooksbarrel exports this state type asAsyncFnState, becauseAsyncStatethere is a different type (see Hook result types).
Gotchas
The wrapped function never rejects. On failure it resolves with whatever was thrown, so
try { await run() } catch {}never catches. Checkresult instanceof Error, or readstate.error.depsdefaults to[], sofnkeeps the props and state of the first render unless you list them.Whatever was thrown lands in
state.erroras it is, even when it is not anError(a string, for example), despite the type.
Related
Applies or removes a discount coupon on the shopper's cart through the Salla SDK, with loading and error state.
useWishlistThe shopper's wishlist, shared by every component on the page: check a product, and add, remove or toggle it through the Salla SDK.
Hook result typesShared shapes behind the action hooks: loading and error state, actions resolving to success, and the wishlist, like and coupon results.
Source and docs
- Engine source:
packages/theme-engine/src/hooks/useAsyncFn.ts