React component for declarative promise resolution and data fetching. Leverages the Render Props pattern 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 any (native) promise
- Choose between Render Props and Context-based helper components
- Provides convenient
isLoading
,startedAt
andfinishedAt
metadata - Provides
cancel
andreload
actions - Automatic re-run using
watch
prop - Accepts
onResolve
andonReject
callbacks - Supports optimistic updates using
setData
- Supports server-side rendering through
initialValue
- Works well in React Native too!
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 thereact-async
package name could be repurposed. The first version of<Async>
is v3.0.0.
<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).
<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.
<Async>
is promise-based, so you can resolve anything you want, not just fetch
requests.
npm install --save react-async
Using render props for ultimate flexibility:
import Async from "react-async"
const loadJson = () => fetch("/some/url").then(res => res.json())
const MyComponent = () => (
<Async promiseFn={loadJson}>
{({ 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>
)
Using helper components (don't have to be direct children) for ease of use:
import Async from "react-async"
const loadJson = () => fetch("/some/url").then(res => res.json())
const MyComponent = () => (
<Async promiseFn={loadJson}>
<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>
)
Creating a custom instance of Async, bound to a specific promiseFn:
import { createInstance } from "react-async"
const loadCustomer = ({ customerId }) => fetch(`/api/customers/${customerId}`).then(...)
const AsyncCustomer = createInstance({ promiseFn: loadCustomer })
const MyComponent = () => (
<AsyncCustomer customerId="123">
<AsyncCustomer.Resolved>{customer => `Hello ${customer.name}`}</AsyncCustomer.Resolved>
</AsyncCustomer>
)
Similarly, this allows you to set default onResolve
and onReject
callbacks.
<Async>
takes the following properties:
promiseFn
{() => Promise} A function that returns a promise; invoked incomponentDidMount
andcomponentDidUpdate
; receives props (object) as argumentdeferFn
{() => Promise} A function that returns a promise; invoked only by callingrun
, with arguments being passed through, as well as props (object) as final argumentwatch
{any} Watches this property throughcomponentDidUpdate
and re-runs thepromiseFn
when the value changes (oldValue !== newValue
)initialValue
{any} initial state fordata
orerror
(if instance of Error); useful for server-side renderingonResolve
{Function} Callback function invoked when a promise resolves, receives data as argumentonReject
{Function} Callback function invoked when a promise rejects, receives error as argument
Be aware that updating
promiseFn
will 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 thepromiseFn
value outside of the render method. If you need to pass variables to thepromiseFn
, pass them as additional props to<Async>
, aspromiseFn
will be invoked with these props. Alternatively you can use memoization to avoid unnecessary updates.
<Async>
provides the following render props:
data
{any} last resolved promise value, maintained when new error arriveserror
{Error} rejected promise reason, cleared when new data arrivesinitialValue
{any} the data or error that was provided through theinitialValue
propisLoading
{boolean}true
while a promise is pendingstartedAt
{Date} when the current/last promise was startedfinishedAt
{Date} when the last promise was resolved or rejectedcancel
{Function} ignores the result of the currently pending promiserun
{Function} runs thedeferFn
, passing any arguments providedreload
{Function} re-runs the promise when invoked, using the previous argumentssetData
{Function} setsdata
to the passed value, unsetserror
and cancels any pending promisesetError
{Function} setserror
to the passed value and cancels any pending promise
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>
)
}
}
<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>
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>
static async getInitialProps() {
// Resolve the promise server-side
const sessions = await loadSessions()
return { sessions }
}
render() {
const { sessions } = this.props // injected by getInitialProps
return (
<Async promiseFn={loadSessions} initialValue={sessions}>
{({ 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>
)
}
<Async>
provides several helper components that make your JSX even more declarative.
They don't have to be direct children of <Async>
and you can use the same component several times.
Renders only while the promise is loading.
initial
{boolean} Show only on initial load (data is undefined)children
{Function|Node} Function which receives props object 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>
Renders only when the promise is resolved.
persist
{boolean} Show old data while loading new data. By default it hides as soon as a new promise starts.children
{Function|Node} Render function which receives data and props object or just a plain React node.
<Async.Resolved persist>{data => <pre>{JSON.stringify(data)}</pre>}</Async.Resolved>
<Async.Resolved>{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}</Async.Resolved>
Renders only when the promise is rejected.
persist
{boolean} Show old error while loading new data. By default it hides as soon as a new promise starts.children
{Function|Node} Render function which receives error and props object or just a plain 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).
persist
{boolean} Show until we have data, even while loading or when an error occurred. By default it hides as soon as the promise starts loading.children
{Function|Node} Function which receives props object 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>
Many thanks to Andrey Popp for handing over ownership of react-async
on npm.