React component and hook for declarative promise resolution and data fetching. Leverages the Render Props pattern and Hooks for ultimate flexibility as well as the new Context API for ease of use. Makes it easy to handle loading and error states, without assumptions about the shape of your data or the type of request.
- Zero dependencies
- Works with promises, async/await and the Fetch API
- Choose between Render Props, Context-based helper components or the
useAsyncanduseFetchhooks - Provides convenient
isLoading,startedAtandfinishedAtmetadata - Provides
cancelandreloadactions - Automatic re-run using
watchorwatchFnprop - Accepts
onResolveandonRejectcallbacks - Supports abortable fetch by providing an AbortController
- Supports optimistic updates using
setData - Supports server-side rendering through
initialValue - Comes with type definitions for TypeScript
- Works well in React Native too!
When upgrading to React Async v4, please note the following breaking API changes:
deferFnnow receives anargsarray as the first argument, instead of arguments torunbeing spread at the front of the arguments list. This enables better interop with TypeScript. You can use destructuring to keep using your existing variables.- The shorthand version of
useAsyncnow takes theoptionsobject as optional second argument. This used to beinitialValue, but was undocumented and inflexible.
- Rationale
- Installation
- Usage
- API
- Helper components
- Usage examples
- Who's using React Async?
- Acknowledgements
React Async is different in that it tries to resolve data as close as possible to where it will be used, while using a declarative syntax, using just JSX and native promises. This is in contrast to systems like Redux where you would configure any data fetching or updates on a higher (application global) level, using a special construct (actions/reducers).
React Async works really well even in larger applications with multiple or nested data dependencies. It encourages loading data on-demand and in parallel at component level instead of in bulk at the route / page level. It's entirely decoupled from your routes, so it works well in complex applications that have a dynamic routing model or don't use routes at all.
React Async is promise-based, so you can resolve anything you want, not just fetch requests.
The React team is currently working on a large rewrite called Concurrent React, previously known as "Async React".
Part of this rewrite is Suspense, which is a generic way for components to suspend rendering while they load data from
a cache. It can render a fallback UI while loading data, much like <Async.Loading>.
React Async has no direct relation to Concurrent React. They are conceptually close, but not the same. React Async is meant to make dealing with asynchronous business logic easier. Concurrent React will make those features have less impact on performance and usability. When Suspense lands, React Async will make full use of Suspense features. In fact you can already start using React Async right now, and in a later update you'll get Suspense features for free.
npm install --save react-async
Or with Yarn:
yarn add react-async
This package requires
reactas a peer dependency. Please make sure to install that as well. If you want to use theuseAsynchook, you'll needreact@16.8.0or later.
React Async offers three primary APIs: the useAsync hook, the <Async> component and the createInstance
factory function. Each has its unique benefits and downsides.
The useAsync hook (available from React v16.8.0) offers direct access to React Async's
core functionality from within your own function components:
import { useAsync } from "react-async"
const loadCustomer = async ({ customerId }, { signal }) => {
const res = await fetch(`/api/customers/${customerId}`, { signal })
if (!res.ok) throw new Error(res)
return res.json()
}
const MyComponent = () => {
const { data, error, isLoading } = useAsync({ promiseFn: loadCustomer, customerId: 1 })
if (isLoading) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
return (
<div>
<strong>Loaded some data:</strong>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
return null
}Or using the shorthand version:
const MyComponent = () => {
const { data, error, isLoading } = useAsync(loadCustomer, options)
// ...
}Because fetch is so commonly used with useAsync, there's a dedicated useFetch hook for it:
import { useFetch } from "react-async"
const MyComponent = () => {
const headers = { Accept: "application/json" }
const { data, error, isLoading, run } = useFetch("/api/example", { headers }, options)
// This will setup a promiseFn with a fetch request and JSON deserialization.
}useFetch takes the same arguments as fetch itself, as well as options to the underlying useAsync hook. The
options object takes two special boolean properties: defer and json. These can be used to switch between
deferFn and promiseFn, and enable JSON parsing. By default useFetch automatically uses promiseFn or deferFn
based on the request method (deferFn for POST / PUT / PATCH / DELETE) and handles JSON parsing if the Accept header
is set to "application/json".
The classic interface to React Async. Simply use directly in your JSX component tree, leveraging the render props pattern:
import Async from "react-async"
// Your promiseFn receives all props from Async and an AbortController instance
const loadCustomer = ({ customerId }, { signal }) =>
fetch(`/api/customers/${customerId}`, { signal })
.then(res => (res.ok ? res : Promise.reject(res)))
.then(res => res.json())
const MyComponent = () => (
<Async promiseFn={loadCustomer} customerId={1}>
{({ data, error, isLoading }) => {
if (isLoading) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
return (
<div>
<strong>Loaded some data:</strong>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
return null
}}
</Async>
)Several helper components are available for better legibility. These don't have to be direct
children of <Async>, because they use Context, offering full flexibility. You can even use render props and helper
components together.
import Async from "react-async"
const loadCustomer = ({ customerId }, { signal }) =>
fetch(`/api/customers/${customerId}`, { signal })
.then(res => (res.ok ? res : Promise.reject(res)))
.then(res => res.json())
const MyComponent = () => (
<Async promiseFn={loadCustomer} customerId={1}>
<Async.Loading>Loading...</Async.Loading>
<Async.Resolved>
{data => (
<div>
<strong>Loaded some data:</strong>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</Async.Resolved>
<Async.Rejected>{error => `Something went wrong: ${error.message}`}</Async.Rejected>
</Async>
)You can also create your own component instances, allowing you to preconfigure them with options such as default
onResolve and onReject callbacks.
import { createInstance } from "react-async"
const loadCustomer = ({ customerId }, { signal }) =>
fetch(`/api/customers/${customerId}`, { signal })
.then(res => (res.ok ? res : Promise.reject(res)))
.then(res => res.json())
// createInstance takes a defaultProps object and a displayName (both optional)
const AsyncCustomer = createInstance({ promiseFn: loadCustomer }, "AsyncCustomer")
const MyComponent = () => (
<AsyncCustomer customerId={1}>
<AsyncCustomer.Resolved>{customer => `Hello ${customer.name}`}</AsyncCustomer.Resolved>
</AsyncCustomer>
)These can be passed in an object to useAsync(), or as props to <Async> and custom instances.
promiseAn already started Promise instance.promiseFnFunction that returns a Promise, automatically invoked.deferFnFunction that returns a Promise, manually invoked withrun.watchWatch a value and automatically reload when it changes.watchFnWatch this function and automatically reload when it returns truthy.initialValueProvide initial data or error for server-side rendering.onResolveCallback invoked when Promise resolves.onRejectCallback invoked when Promise rejects.
useFetch additionally takes these options:
deferForce the use ofdeferFnorpromiseFn.jsonEnable JSON parsing of the response.
Promise
A Promise instance which has already started. It will simply add the necessary resolve/reject callbacks and set
startedAt to the time promise was first provided. Changing the value of promise will cancel any pending promise
and listen to the new one. If promise is initially undefined, the React Async state will be pending.
Note that
reloadwill not do anything when usingpromise. UsepromiseFninstead.
function(props: object, controller: AbortController): Promise
A function that returns a promise. It is automatically invoked in componentDidMount and componentDidUpdate.
The function receives all component props (or options) and an AbortController instance as arguments.
Be aware that updating
promiseFnwill trigger it to cancel any pending promise and load the new promise. Passing an arrow function will cause it to change and reload on every render of the parent component. You can avoid this by defining thepromiseFnvalue outside of the render method. If you need to pass variables to thepromiseFn, pass them as additional props to<Async>, aspromiseFnwill be invoked with these props. Alternatively you can use memoization to avoid unnecessary updates.
function(args: any[], props: object, controller: AbortController): Promise
A function that returns a promise. This is invoked only by manually calling run(...args). Receives the same arguments
as promiseFn, as well as any arguments to run which are passed through as an array. The deferFn is commonly used
to send data to the server following a user action, such as submitting a form. You can use this in conjunction with
promiseFn to fill the form with existing data, then updating it on submit with deferFn.
Be aware that when using both
promiseFnanddeferFn, the shape of their resolved value should match, because they both update the samedata.
any
Watches this property through componentDidUpdate and re-runs the promiseFn when the value changes, using a simple
reference check (oldValue !== newValue). If you need a more complex update check, use watchFn instead.
function(props: object, prevProps: object): boolean | any
Re-runs the promiseFn when this callback returns truthy (called on every update). Any default props specified by
createInstance are available too.
any | Error
Initial state for data or error (if instance of Error); useful for server-side rendering.
function(data: any): void
Callback function invoked when a promise resolves, receives data as argument.
function(reason: Error): void
Callback function invoked when a promise rejects, receives rejection reason (error) as argument.
boolean
Enables the use of deferFn if true, or enables the use of promiseFn if false. By default this is automatically
chosen based on the request method (deferFn for POST / PUT / PATCH / DELETE, promiseFn otherwise).
boolean
Enables or disables JSON parsing of the response body. By default this is automatically enabled if the Accept header
is set to "application/json".
<Async> provides the following render props to the children function:
dataLast resolved promise value, maintained when new error arrives.errorRejected promise reason, cleared when new data arrives.initialValueThe data or error that was provided through theinitialValueprop.isLoadingWhether or not a Promise is currently pending.startedAtWhen the current/last promise was started.finishedAtWhen the last promise was resolved or rejected.counterThe number of times a promise was started.cancelCancel any pending promise.runInvokes thedeferFn.reloadRe-runs the promise when invoked, using the any previous arguments.setDataSetsdatato the passed value, unsetserrorand cancels any pending promise.setErrorSetserrorto the passed value and cancels any pending promise.
any
Last resolved promise value, maintained when new error arrives.
Error
Rejected promise reason, cleared when new data arrives.
any | Error
The data or error that was originally provided through the initialValue prop.
boolean
true while a promise is pending, false otherwise.
Date
Tracks when the current/last promise was started.
Date
Tracks when the last promise was resolved or rejected.
number
The number of times a promise was started.
function(): void
Cancels the currently pending promise by ignoring its result and calls abort() on the AbortController.
function(...args: any[]): Promise
Runs the deferFn, passing any arguments provided as an array.
function(): void
Re-runs the promise when invoked, using the previous arguments.
function(data: any, callback?: () => void): any
Function that sets data to the passed value, unsets error and cancels any pending promise. Takes an optional
callback which is invoked after the state update is completed. Returns the data to enable chaining.
function(error: Error, callback?: () => void): Error
Function that sets error to the passed value and cancels any pending promise. Takes an optional callback which is
invoked after the state update is completed. Returns the error to enable chaining.
React Async provides several helper components that make your JSX more declarative and less cluttered.
They don't have to be direct children of <Async> and you can use the same component several times.
This component renders only while the promise is loading (unsettled).
initialbooleanShow only on initial load (whendataisundefined).childrenfunction(state: object): Node | NodeRender function or React Node.
<Async.Loading initial>
<p>This text is only rendered while performing the initial load.</p>
</Async.Loading><Async.Loading>{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}</Async.Loading>This component renders only when the promise is fulfilled with data (data !== undefined).
persistbooleanShow old data while loading new data. By default it hides as soon as a new promise starts.childrenfunction(data: any, state: object): Node | NodeRender function or React Node.
<Async.Resolved persist>{data => <pre>{JSON.stringify(data)}</pre>}</Async.Resolved><Async.Resolved>{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}</Async.Resolved>This component renders only when the promise is rejected.
persistbooleanShow old error while loading new data. By default it hides as soon as a new promise starts.childrenfunction(error: Error, state: object): Node | NodeRender function or React Node.
<Async.Rejected persist>Oops.</Async.Rejected><Async.Rejected>{error => `Unexpected error: ${error.message}`}</Async.Rejected>Renders only while the deferred promise is still pending (not yet run), or you have not provided any promise.
persistbooleanShow until we have data, even while loading or when an error occurred. By default it hides as soon as the promise starts loading.childrenfunction(state: object): Node | NodeRender function or React Node.
<Async deferFn={deferFn}>
<Async.Pending>
<p>This text is only rendered while `run` has not yet been invoked on `deferFn`.</p>
</Async.Pending>
</Async><Async.Pending persist>
{({ error, isLoading, run }) => (
<div>
<p>This text is only rendered while the promise has not resolved yet.</p>
<button onClick={run} disabled={!isLoading}>
Run
</button>
{error && <p>{error.message}</p>}
</div>
)}
</Async.Pending>Here's several examples to give you an idea of what's possible with React Async. For fully working examples, please
check out the examples directory.
This does some basic data fetching, including a loading indicator, error state and retry.
class App extends Component {
getSession = ({ sessionId }) => fetch(...)
render() {
// The promiseFn should be defined outside of render()
return (
<Async promiseFn={this.getSession} sessionId={123}>
{({ data, error, isLoading, reload }) => {
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return (
<div>
<p>{error.toString()}</p>
<button onClick={reload}>try again</button>
</div>
)
}
if (data) {
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
return null
}}
</Async>
)
}
}This uses deferFn to trigger an update (e.g. POST / PUT request) after a form submit.
const subscribeToNewsletter = (args, props, controller) => fetch(...)
<Async deferFn={subscribeToNewsletter}>
{({ error, isLoading, run }) => (
<form onSubmit={run}>
<input type="email" name="email" />
<button type="submit" disabled={isLoading}>
Subscribe
</button>
{error && <p>{error.toString()}</p>}
</form>
)}
</Async>This uses both promiseFn and deferFn along with setData to implement optimistic updates.
const updateAttendance = ([attend]) => fetch(...).then(() => attend, () => !attend)
<Async promiseFn={getAttendance} deferFn={updateAttendance}>
{({ data: isAttending, isLoading, run, setData }) => (
<Toggle
on={isAttending}
onClick={() => {
setData(!isAttending)
run(!isAttending)
}}
disabled={isLoading}
/>
)}
</Async>This uses initialValue to enable server-side rendering with Next.js.
static async getInitialProps() {
// Resolve the promise server-side
const customers = await loadCustomers()
return { customers }
}
render() {
const { customers } = this.props // injected by getInitialProps
return (
<Async promiseFn={loadCustomers} initialValue={customers}>
{({ data, error, isLoading, initialValue }) => { // initialValue is passed along for convenience
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <p>{error.toString()}</p>
}
if (data) {
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
return null
}}
</Async>
)
}Your organization here? Let us know you're using React Async!
Versions 1.x and 2.x of react-async on npm are from a different project abandoned years ago. The original author was
kind enough to transfer ownership so the react-async package name could be repurposed. The first version of this
project is v3.0.0. Many thanks to Andrey Popp for handing over ownership of react-async on npm.
