react-async-hook
A tiny React hook library for handling async operations, mutations, and cancellations without race conditions.
Repository Health
Technical Analysis
React-Async-Hook is a minimal library providing useAsync, useAsyncCallback, and useAsyncAbortable hooks for wiring any async function into React component state. It tracks loading/error/result status, guards against race conditions by only committing the result of the most recently fired call, and works with any promise-returning function — not tied to fetch or axios.
Deliberately scoped to do one thing well: unlike SWR or React-Query, it has no caching, deduplication, or stale-while-revalidate layer — it’s meant as a lightweight primitive you can compose with your own debouncing/caching logic (the author’s own awesome-debounce-promise is a common pairing) or use directly for simple loading-state UI needs like buttons and forms.
What You Get
- useAsync for auto-executing async calls on mount and whenever dependency params change
- useAsyncCallback for manually triggered async actions (form submits, button clicks) with loading/error state
- useAsyncAbortable for wiring an AbortSignal into the async function so in-flight requests are cancelled when params change
- Configurable state transitions via setLoading/setResult/setError/onSuccess/onError options for custom merge strategies
- Full TypeScript typings with generics for result and argument types, tree-shakable CJS/ESM builds
Common Use Cases
- Loading an async resource (e.g. a detail view) into a component with automatic race-condition-safe updates on prop changes
- Wiring an async onClick handler to a button so it shows a loading state and disables itself for the duration of the request
- Building a debounced search/autocomplete input by composing useAsyncAbortable with a debounced fetch function
- Manual refetch/refresh flows, calling execute() again without needing to change params
- Keeping previous results visible during a refetch by customizing the setLoading merge strategy
Under The Hood
Architecture The library is a single-file module (src/index.ts) built around a layered composition of small internal hooks: useGetter (a stable ref-based getter that avoids stale closures), useAsyncState (wraps useState with configurable setLoading/setResult/setError/merge/reset transitions), useIsMounted (a mounted-ref guard), and useCurrentPromise (tracks the in-flight promise by reference). useAsyncInternal composes all four into the core execution engine: each call wraps the target function in an async IIFE, stores it as the current promise, transitions state to loading, and on settle only commits the result if that promise is still current and the component is still mounted, discarding out-of-order responses to prevent race conditions. useAsync, useAsyncCallback, and useAsyncAbortable are thin public wrappers over useAsyncInternal — useAsyncCallback disables the mount/update auto-execute effect, and useAsyncAbortable wraps the async function to inject and abort an AbortController before the internal engine ever sees it. This composition means the entire public surface funnels through one code path, so any change to useAsyncInternal’s core affects all three hooks identically.
Tech Stack TypeScript targeting React (peerDependency >=16.8), built with tsdx (a zero-config TS library bundler wrapping Rollup + Babel), producing CommonJS and ESM builds plus type declarations. No runtime dependencies beyond React itself. Testing uses Jest via tsdx test with @testing-library/react-hooks and @testing-library/jest-dom for hook-level assertions, ts-jest for TS transpilation. Formatting is enforced by Prettier plus a husky pre-commit hook running pretty-quick. CI is limited to a dormant Travis config, consistent with the project’s low recent activity.
Code Quality A single test file exercises useAsync via renderHook/waitForNextUpdate across a wide range of cases — basic resolve, param-triggered re-fetch, the no-deps-array shortcut, out-of-order race conditions, Jest-mocked resolved values, synchronous return values, and both synchronously-thrown and rejected-async error paths — but useAsyncCallback and useAsyncAbortable have no dedicated tests. Source uses strict TypeScript generics throughout, with only a handful of isolated escapes, each accompanied by a comment explaining exactly why. Naming is consistent and self-documenting. Error handling relies on native Promise rejection wrapped in an async IIFE specifically so synchronous throws are caught too. No dedicated linter config is present beyond tsdx’s bundled defaults.
API Design The three-hook surface is intentionally minimal — useAsync auto-executes and re-executes on deps changes much like useEffect, useAsyncCallback flips that off for manual/click-triggered flows, and useAsyncAbortable layers an AbortSignal in front of either — so callers only ever need to learn one state shape (status/loading/error/result plus execute/reset/set/merge). Race-condition safety and cancellation-by-default are handled internally rather than pushed onto the caller, which is the library’s main developer-experience differentiator against hand-rolled useEffect-plus-useState fetch patterns; the README explicitly positions it as a smaller-footprint alternative to SWR and React-Query for teams that don’t need caching, deduplication, or revalidation. It’s documented through inline README code samples covering debounce, cancellation, refetch, and preserve-previous-result patterns rather than a separate docs site.