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:

  • error is updated
  • loading resets
  • existing value remains 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:

TypeExample
stringAPI tokens
numbermetrics
Useruser objects
WeatherPoint[]forecast arrays
voidside-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

ParameterTypeDescription
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