Skip to content

Commit

Permalink
Update API to better match Promise spec and add the 'status' prop (cl…
Browse files Browse the repository at this point in the history
…oses #35) (#37)

+ **Breaking change**: `Async.Pending` was renamed to `Async.Waiting`
+ Added the `status` prop, which can be one of `initial`, `pending`, `fulfilled` or `rejected`
+ Added `isInitial`, `isPending`, `isFulfilled` (with alias `isResolved`), `isRejected` and `isSettled` boolean props. `isLoading` is now an alias for `isPending`.
+ Added separate TypeScript types for each status, to make various props non-optional.

The `pending` and `fulfilled` statuses were chosen over `loading` and `resolved` because they better match the [terminology in the Promise specification](https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md#readme). The `Pending` helper component was renamed to `Initial` accordingly, causing a breaking change.
  • Loading branch information
ghengeveld authored Mar 29, 2019
2 parents 3d450b4 + 4cdf0ab commit c4fe8eb
Show file tree
Hide file tree
Showing 20 changed files with 424 additions and 190 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ node_js:
cache:
directories:
- node_modules
script: npm run test:compat
script: npm run lint && npm run test:compat
after_success:
- bash <(curl -s https://codecov.io/bash) -e TRAVIS_NODE_VERSION
135 changes: 94 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,14 @@ is set to `"application/json"`.
- `data` Last resolved promise value, maintained when new error arrives.
- `error` Rejected promise reason, cleared when new data arrives.
- `initialValue` The data or error that was provided through the `initialValue` prop.
- `isLoading` Whether or not a Promise is currently pending.
- `startedAt` When the current/last promise was started.
- `finishedAt` When the last promise was resolved or rejected.
- `status` One of: `initial`, `pending`, `fulfilled`, `rejected`.
- `isInitial` true when no promise has ever started, or one started but was cancelled.
- `isPending` true when a promise is currently awaiting settlement. Alias: `isLoading`
- `isFulfilled` true when the last promise was fulfilled with a value. Alias: `isResolved`
- `isRejected` true when the last promise was rejected with a reason.
- `isSettled` true when the last promise was fulfilled or rejected (not initial or pending).
- `counter` The number of times a promise was started.
- `cancel` Cancel any pending promise.
- `run` Invokes the `deferFn`.
Expand All @@ -410,12 +415,6 @@ Rejected promise reason, cleared when new data arrives.
The data or error that was originally provided through the `initialValue` prop.

#### `isLoading`

> `boolean`
`true` while a promise is pending, `false` otherwise.

#### `startedAt`

> `Date`
Expand All @@ -428,6 +427,47 @@ Tracks when the current/last promise was started.
Tracks when the last promise was resolved or rejected.

#### `status`

> `string`
One of: `initial`, `pending`, `fulfilled`, `rejected`.
These are available for import as `statusTypes`.

#### `isInitial`

> `boolean`
`true` while no promise has started yet, or one was started but cancelled.

#### `isPending`

> `boolean`
`true` while a promise is pending (loading), `false` otherwise.

Alias: `isLoading`

#### `isFulfilled`

> `boolean`
`true` when the last promise was fulfilled (resolved) with a value.

Alias: `isResolved`

#### `isRejected`

> `boolean`
`true` when the last promise was rejected with an error.

#### `isSettled`

> `boolean`
`true` when the last promise was either fulfilled or rejected (i.e. not initial or pending)

#### `counter`

> `number`
Expand Down Expand Up @@ -471,10 +511,45 @@ invoked after the state update is completed. Returns the error to enable chainin
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.

### `<Async.Loading>`
### `<Async.Initial>`

Renders only while the deferred promise is still waiting to be run, or you have not provided any promise.

#### Props

- `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(state: object): Node | Node` Render function or React Node.

#### Examples

```js
<Async deferFn={deferFn}>
<Async.Initial>
<p>This text is only rendered while `run` has not yet been invoked on `deferFn`.</p>
</Async.Initial>
</Async>
```

```js
<Async.Initial 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.Initial>
```

### `<Async.Pending>`

This component renders only while the promise is loading (unsettled).

Alias: `<Async.Loading>`

#### Props

- `initial` `boolean` Show only on initial load (when `data` is `undefined`).
Expand All @@ -483,19 +558,21 @@ This component renders only while the promise is loading (unsettled).
#### Examples

```js
<Async.Loading initial>
<Async.Pending initial>
<p>This text is only rendered while performing the initial load.</p>
</Async.Loading>
</Async.Pending>
```

```js
<Async.Loading>{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}</Async.Loading>
<Async.Pending>{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}</Async.Pending>
```

### `<Async.Resolved>`
### `<Async.Fulfilled>`

This component renders only when the promise is fulfilled with data (`data !== undefined`).

Alias: `<Async.Resolved>`

#### Props

- `persist` `boolean` Show old data while loading new data. By default it hides as soon as a new promise starts.
Expand All @@ -504,11 +581,11 @@ This component renders only when the promise is fulfilled with data (`data !== u
#### Examples

```js
<Async.Resolved persist>{data => <pre>{JSON.stringify(data)}</pre>}</Async.Resolved>
<Async.Fulfilled persist>{data => <pre>{JSON.stringify(data)}</pre>}</Async.Fulfilled>
```

```js
<Async.Resolved>{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}</Async.Resolved>
<Async.Fulfilled>{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}</Async.Fulfilled>
```

### `<Async.Rejected>`
Expand All @@ -530,39 +607,15 @@ This component renders only when the promise is rejected.
<Async.Rejected>{error => `Unexpected error: ${error.message}`}</Async.Rejected>
```

### `<Async.Pending>`
### `<Async.Settled>`

Renders only while the deferred promise is still pending (not yet run), or you have not provided any promise.
This component renders only when the promise is fulfilled or rejected.

#### Props

- `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.
- `persist` `boolean` Show old data or error while loading new data. By default it hides as soon as a new promise starts.
- `children` `function(state: object): Node | Node` Render function or React Node.

#### Examples

```js
<Async deferFn={deferFn}>
<Async.Pending>
<p>This text is only rendered while `run` has not yet been invoked on `deferFn`.</p>
</Async.Pending>
</Async>
```

```js
<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>
```

## Usage examples

Here's several examples to give you an idea of what's possible with React Async. For fully working examples, please
Expand Down
10 changes: 5 additions & 5 deletions examples/basic-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ const UserDetails = ({ data }) => (
const App = () => (
<>
<Async promiseFn={loadUser} userId={1}>
{({ data, error, isLoading }) => {
if (isLoading) return <UserPlaceholder />
{({ data, error, isPending }) => {
if (isPending) return <UserPlaceholder />
if (error) return <p>{error.message}</p>
if (data) return <UserDetails data={data} />
return null
}}
</Async>

<Async promiseFn={loadUser} userId={2}>
<Async.Loading>
<Async.Pending>
<UserPlaceholder />
</Async.Loading>
<Async.Resolved>{data => <UserDetails data={data} />}</Async.Resolved>
</Async.Pending>
<Async.Fulfilled>{data => <UserDetails data={data} />}</Async.Fulfilled>
<Async.Rejected>{error => <p>{error.message}</p>}</Async.Rejected>
</Async>
</>
Expand Down
4 changes: 2 additions & 2 deletions examples/basic-hook/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ const UserDetails = ({ data }) => (
)

const User = ({ userId }) => {
const { data, error, isLoading } = useAsync({ promiseFn: loadUser, userId })
if (isLoading) return <UserPlaceholder />
const { data, error, isPending } = useAsync({ promiseFn: loadUser, userId })
if (isPending) return <UserPlaceholder />
if (error) return <p>{error.message}</p>
if (data) return <UserDetails data={data} />
return null
Expand Down
10 changes: 5 additions & 5 deletions examples/custom-instance/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ const UserDetails = ({ data }) => (
const App = () => (
<>
<AsyncUser userId={1}>
{({ data, error, isLoading }) => {
if (isLoading) return <UserPlaceholder />
{({ data, error, isPending }) => {
if (isPending) return <UserPlaceholder />
if (error) return <p>{error.message}</p>
if (data) return <UserDetails data={data} />
return null
}}
</AsyncUser>

<AsyncUser userId={2}>
<AsyncUser.Loading>
<AsyncUser.Pending>
<UserPlaceholder />
</AsyncUser.Loading>
<AsyncUser.Resolved>{data => <UserDetails data={data} />}</AsyncUser.Resolved>
</AsyncUser.Pending>
<AsyncUser.Fulfilled>{data => <UserDetails data={data} />}</AsyncUser.Fulfilled>
<AsyncUser.Rejected>{error => <p>{error.message}</p>}</AsyncUser.Rejected>
</AsyncUser>
</>
Expand Down
22 changes: 11 additions & 11 deletions examples/movie-app/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ const TopMovies = ({ handleSelect }) => (
</span>
</h1>
<Async promiseFn={fetchMovies}>
<Async.Loading>
<Async.Pending>
<p>Loading...</p>
</Async.Loading>
<Async.Resolved>
</Async.Pending>
<Async.Fulfilled>
{movies =>
movies.map(movie => <Movie {...movie} key={movie.id} onSelect={handleSelect(movie)} />)
}
</Async.Resolved>
</Async.Fulfilled>
</Async>
</Fragment>
)
Expand Down Expand Up @@ -99,10 +99,10 @@ const Details = ({ onBack, id }) => (
</span>
</button>
<Async promiseFn={fetchMovieDetails} id={id} onResolve={console.log}>
<Async.Loading>
<Async.Pending>
<p>Loading...</p>
</Async.Loading>
<Async.Resolved>
</Async.Pending>
<Async.Fulfilled>
{movie => (
<Fragment>
<div className="main">
Expand All @@ -126,15 +126,15 @@ const Details = ({ onBack, id }) => (
</div>
<div className="reviews">
<Async promiseFn={fetchMovieReviews} id={id} onResolve={console.log}>
<Async.Loading>
<Async.Pending>
<p>Loading...</p>
</Async.Loading>
<Async.Resolved>{reviews => reviews.map(Review)}</Async.Resolved>
</Async.Pending>
<Async.Fulfilled>{reviews => reviews.map(Review)}</Async.Fulfilled>
</Async>
</div>
</Fragment>
)}
</Async.Resolved>
</Async.Fulfilled>
</Async>
</div>
)
Expand Down
6 changes: 3 additions & 3 deletions examples/with-abortcontroller/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ const download = (args, props, controller) =>
.then(res => res.json())

const App = () => {
const { run, cancel, isLoading } = useAsync({ deferFn: download })
const { run, cancel, isPending } = useAsync({ deferFn: download })
return (
<>
{isLoading ? <button onClick={cancel}>cancel</button> : <button onClick={run}>start</button>}
{isLoading ? (
{isPending ? <button onClick={cancel}>cancel</button> : <button onClick={run}>start</button>}
{isPending ? (
<p>Loading...</p>
) : (
<p>Inspect network traffic to see requests being canceled.</p>
Expand Down
8 changes: 4 additions & 4 deletions examples/with-nextjs/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ class Hello extends React.Component {
const { userId = 1 } = url.query
return (
<Async promiseFn={loadUser} userId={userId} watch={userId} initialValue={data}>
<Async.Loading>
<Async.Pending>
<p>Loading...</p>
</Async.Loading>
<Async.Resolved>
</Async.Pending>
<Async.Fulfilled>
{data => (
<>
<p>
Expand All @@ -43,7 +43,7 @@ class Hello extends React.Component {
</p>
</>
)}
</Async.Resolved>
</Async.Fulfilled>
<i>
This data is initially loaded server-side, then client-side when navigating prev/next.
</i>
Expand Down
6 changes: 3 additions & 3 deletions examples/with-react-router/js/Contributors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"

const Contributors = ({ data, error, isLoading }) => {
if (isLoading) return "Loading Contributers..."
const Contributors = ({ data, error, isPending }) => {
if (isPending) return "Loading Contributers..."
if (error) return "Error"
return (
<ul>
Expand All @@ -12,4 +12,4 @@ const Contributors = ({ data, error, isLoading }) => {
)
}

export default Contributors
export default Contributors
Loading

0 comments on commit c4fe8eb

Please sign in to comment.