hooks/useAsync.ts
Purpose
The useAsync hook provides a lightweight abstraction for managing asynchronous operations in React components.
It is responsible for:
- executing async functions
- tracking loading state
- capturing execution errors
- storing resolved values
- exposing a reusable async execution API
The hook simplifies async UI state management while preserving generic typing support.
Architecture Overview
The hook implements:
- generic async execution support
- reactive loading/error/value state
- memoized async execution
- centralized error normalization
The core execution flow is encapsulated in:
run()
which manages the full lifecycle of an async operation.
Invariants
The hook maintains the following invariants.
1. Loading State Reflects Active Execution
Before async execution begins:
setLoading(true)
is always called.
After execution completes — whether successful or failed:
setLoading(false)
is guaranteed via:
finally
This ensures loading state always accurately reflects execution lifecycle.
2. Errors Reset Before Each Execution
Before invoking the async function:
setError(null)
clears any previous error state.
This guarantees stale errors do not persist across retries.
3. Successful Executions Update value
If the async operation resolves successfully:
setValue(v)
stores the resolved value.
The hook always reflects the most recent successful execution result.
4. Failed Executions Preserve Previous Value
On failure:
erroris updatedloadingresets- existing
valueremains unchanged
This allows UI consumers to continue rendering previously successful data during transient failures.
5. Errors Are Normalized to Strings
Caught errors are normalized through:
e?.message ?? String(e)
This guarantees error is always:
string | null
regardless of thrown error type.
6. The Async Function Is Memoized
The execution wrapper is memoized through:
React.useCallback(...)
with dependency:
[fn]
This guarantees stable function identity unless the supplied async function changes.
Variants
Generic Type Variants
The hook is generic:
useAsync<T>()
allowing any async return type.
Examples:
| Type | Example |
|---|---|
string | API tokens |
number | metrics |
User | user objects |
WeatherPoint[] | forecast arrays |
void | side-effect-only operations |
Execution Variants
Successful Execution
State transitions:
loading: true
error: null
value: resolved value
loading: false
Failed Execution
State transitions:
loading: true
error: normalized message
loading: false
Value Lifecycle Variants
Initial State
value = null
Post-Success State
value = resolved async result
Post-Failure State
The previous successful value is preserved.
Exported Functions
useAsync(fn)
export function useAsync<T>(
fn: () => Promise<T>
)
Creates managed async execution state around an async function.
Parameters
| Parameter | Type | Description |
|---|---|---|
fn | () => Promise<T> | Async operation to execute |
Return Value
{
run,
loading,
error,
value,
setValue
}
Returned Properties
run
run(): Promise<T>
Executes the async operation.
Responsibilities
- resets error state
- toggles loading state
- stores resolved value
- captures execution errors
- rethrows original errors
Example
await run();
loading
boolean
Indicates whether async execution is currently active.
Example
if (loading) {
return <Spinner />;
}
error
string | null
Contains the normalized error message from the most recent failed execution.
Example
{error && <Text>{error}</Text>}
value
T | null
Stores the most recent successful async result.
Example
<Text>{value?.name}</Text>
setValue
React.Dispatch<React.SetStateAction<T | null>>
Allows manual mutation of stored value state.
Common Use Cases
- optimistic UI updates
- local cache edits
- partial mutations
- manual reset flows
Example
setValue(null);
Internal Behavior
Execution Flow
run()
→ setLoading(true)
→ clear previous error
→ await async function
→ store result OR capture error
→ setLoading(false)
Error Flow
throw
→ normalize error message
→ update error state
→ rethrow original error
Error Handling Strategy
The hook intentionally:
- stores normalized string errors for UI display
- rethrows original errors for upstream handling
This enables both:
- declarative UI state management
- imperative error control flows
State Lifecycle
Initial State
loading = false
error = null
value = null
During Execution
loading = true
After Success
loading = false
error = null
value = resolved value
After Failure
loading = false
error = normalized message
Previous value remains unchanged.
Common Use Cases
The hook is suitable for:
- API requests
- async database reads
- mutations
- background task triggers
- sensor initialization
- authentication flows
- lazy loading
- cache refresh operations
Concurrency Notes
The hook does not serialize executions.
Multiple concurrent run() calls may:
- overlap
- resolve out of order
- overwrite each other’s state
Consumers requiring strict execution ordering should implement additional coordination.
Performance Notes
Because run is memoized:
React.useCallback(...)
it is safe to:
- pass into child props
- use in dependency arrays
- attach to event handlers
without unnecessary recreation unless fn changes.
Example Integration
const {
run,
loading,
error,
value
} = useAsync(async () => {
return fetchUserProfile();
});
useEffect(() => {
run();
}, [run]);
Example Async Flow
button press
→ run()
→ loading spinner shown
→ async operation resolves
→ value updated
→ spinner hidden