React hooks and components for managing async state
- Declarative async state management for React
- Hooks for manual (
useAsync) and automatic (useAutoAsync) async actions - Component (
AsyncGuard) for rendering UI based on async state - Automatic request cancellation via
AbortControlleron unmount or variable changes - Fully typed discriminated union states with automatic type narrowing
npm i react @types/react @recrafter/asyncimport React from 'react';
import {useAutoAsync, AsyncGuard} from '@recrafter/async';
// Simulate an async function
const fetchData = async ({
variables,
signal,
}: {
variables: {id: number};
signal: AbortSignal;
}) => {
const response = await fetch(`/api/items/${variables.id}`, {signal});
return response.json() as Promise<{message: string}>;
};
export function Example() {
const [asyncState, asyncActions] = useAutoAsync({
createPromise: fetchData,
variables: {id: 1},
});
return (
<AsyncGuard
{...asyncState}
loadingSlot={<div>Loading...</div>}
loadingFailureSlot={({reason}) => (
<div>Error: {String(reason)}</div>
)}
children={({data}) => <div>Result: {data.message}</div>}
/>
);
}Manual control over async actions.
const [asyncState, asyncActions] = useAsync({
createPromise,
isSkipped, // optional
initialAsyncState, // optional
});createPromise: ({ variables, signal }) => PromiseisSkipped: boolean (optional)initialAsyncState: AsyncState (optional)
Actions:
asyncActions.start({ variables, onSuccess, onFailure, onFinally })(onSuccess,onFailure,onFinallyare optional)asyncActions.abort()
// Start (or restart) the request manually
asyncActions.start({
variables: {id: 2},
onSuccess: (data) => console.log(data),
});
// Cancel the in-flight request
asyncActions.abort();Automatically runs the async action on mount and when variables change.
const [asyncState, asyncActions] = useAutoAsync({
createPromise,
variables,
isSkipped, // optional
initialAsyncState, // optional
onSuccess, // optional
onFailure, // optional
onFinally, // optional
});createPromise: ({ variables, signal }) => Promisevariables:Variables(same type as increatePromise; used as a dependency)isSkipped,initialAsyncState,onSuccess,onFailure,onFinally: optional
Actions:
asyncActions.restart(updateArgs?)asyncActions.abort()
// Re-run the request with the same variables (e.g. a "retry" button)
asyncActions.restart();
// Re-run with overridden callbacks for this call only
asyncActions.restart({onSuccess: (data) => console.log(data)});
// Cancel the in-flight request
asyncActions.abort();Component for rendering different UI based on async state.
<AsyncGuard
{...asyncState}
loadingSlot={<div>Loading...</div>}
loadingFailureSlot={({reason}) => <div>Error: {String(reason)}</div>}
children={({data}) => <div>Result: {data}</div>}
/>children: Render on success (status: SUCCESS)loadingSlot,loadingFailureSlot,loadingAbortSlot,updatingSlot,updatingFailureSlot,updatingAbortSlot,skipSlot
Each slot can be a React element or a render function.
The async state returned by hooks is a discriminated union on the status
field:
status: SKIP— Skippedstatus: LOADING— Loadingstatus: SUCCESS— Success, hasdatastatus: UPDATING— Updating, hasdatastatus: LOADING_FAILURE— Loading failed, hasreasonstatus: UPDATING_FAILURE— Updating failed, hasdataandreasonstatus: LOADING_ABORT— Loading abortedstatus: UPDATING_ABORT— Updating aborted, hasdata
Each state also has flags: isWaiting, hasData, isFailed, isAborted.
Because it's a discriminated union, narrowing on status (or on a flag like
hasData) lets TypeScript infer which fields are available — no optional
chaining or manual casts needed:
if (asyncState.status === ASYNC_STATUS.SUCCESS) {
asyncState.data; // typed as Data, no cast needed
}
if (asyncState.hasData) {
asyncState.data; // also narrowed here — true for SUCCESS, UPDATING,
// UPDATING_FAILURE and UPDATING_ABORT
}MIT
