diff --git a/.travis.yml b/.travis.yml
index 2286c93e..8fe9667c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/README.md b/README.md
index 935196f8..13a2a2b8 100644
--- a/README.md
+++ b/README.md
@@ -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`.
@@ -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`
@@ -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`
@@ -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 `` and you can use the same component several times.
-### ``
+### ``
+
+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
+
+
+ This text is only rendered while `run` has not yet been invoked on `deferFn`.
+
+
+```
+
+```js
+
+ {({ error, isLoading, run }) => (
+
+
This text is only rendered while the promise has not resolved yet.
+
+ Run
+
+ {error &&
{error.message}
}
+
+ )}
+
+```
+
+### ``
This component renders only while the promise is loading (unsettled).
+Alias: ``
+
#### Props
- `initial` `boolean` Show only on initial load (when `data` is `undefined`).
@@ -483,19 +558,21 @@ This component renders only while the promise is loading (unsettled).
#### Examples
```js
-
+
This text is only rendered while performing the initial load.
-
+
```
```js
-{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}
+{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}
```
-### ``
+### ``
This component renders only when the promise is fulfilled with data (`data !== undefined`).
+Alias: ``
+
#### Props
- `persist` `boolean` Show old data while loading new data. By default it hides as soon as a new promise starts.
@@ -504,11 +581,11 @@ This component renders only when the promise is fulfilled with data (`data !== u
#### Examples
```js
-{data => {JSON.stringify(data)} }
+{data => {JSON.stringify(data)} }
```
```js
-{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}
+{({ finishedAt }) => `Last updated ${startedAt.toISOString()}`}
```
### ``
@@ -530,39 +607,15 @@ This component renders only when the promise is rejected.
{error => `Unexpected error: ${error.message}`}
```
-### ``
+### ``
-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
-
-
- This text is only rendered while `run` has not yet been invoked on `deferFn`.
-
-
-```
-
-```js
-
- {({ error, isLoading, run }) => (
-
-
This text is only rendered while the promise has not resolved yet.
-
- Run
-
- {error &&
{error.message}
}
-
- )}
-
-```
-
## Usage examples
Here's several examples to give you an idea of what's possible with React Async. For fully working examples, please
diff --git a/examples/basic-fetch/src/index.js b/examples/basic-fetch/src/index.js
index 0d903442..f138c1d5 100644
--- a/examples/basic-fetch/src/index.js
+++ b/examples/basic-fetch/src/index.js
@@ -27,8 +27,8 @@ const UserDetails = ({ data }) => (
const App = () => (
<>
- {({ data, error, isLoading }) => {
- if (isLoading) return
+ {({ data, error, isPending }) => {
+ if (isPending) return
if (error) return {error.message}
if (data) return
return null
@@ -36,10 +36,10 @@ const App = () => (
-
+
-
- {data => }
+
+ {data => }
{error => {error.message}
}
>
diff --git a/examples/basic-hook/src/index.js b/examples/basic-hook/src/index.js
index 81e27b82..d9991912 100644
--- a/examples/basic-hook/src/index.js
+++ b/examples/basic-hook/src/index.js
@@ -26,8 +26,8 @@ const UserDetails = ({ data }) => (
)
const User = ({ userId }) => {
- const { data, error, isLoading } = useAsync({ promiseFn: loadUser, userId })
- if (isLoading) return
+ const { data, error, isPending } = useAsync({ promiseFn: loadUser, userId })
+ if (isPending) return
if (error) return
{error.message}
if (data) return
return null
diff --git a/examples/custom-instance/src/index.js b/examples/custom-instance/src/index.js
index ece5d50e..61d08af2 100644
--- a/examples/custom-instance/src/index.js
+++ b/examples/custom-instance/src/index.js
@@ -29,8 +29,8 @@ const UserDetails = ({ data }) => (
const App = () => (
<>
- {({ data, error, isLoading }) => {
- if (isLoading) return
+ {({ data, error, isPending }) => {
+ if (isPending) return
if (error) return {error.message}
if (data) return
return null
@@ -38,10 +38,10 @@ const App = () => (
-
+
-
- {data => }
+
+ {data => }
{error => {error.message}
}
>
diff --git a/examples/movie-app/src/App.js b/examples/movie-app/src/App.js
index 8e32096a..72058217 100755
--- a/examples/movie-app/src/App.js
+++ b/examples/movie-app/src/App.js
@@ -62,14 +62,14 @@ const TopMovies = ({ handleSelect }) => (
-
+
Loading...
-
-
+
+
{movies =>
movies.map(movie => )
}
-
+
)
@@ -99,10 +99,10 @@ const Details = ({ onBack, id }) => (
-
+
Loading...
-
-
+
+
{movie => (
@@ -126,15 +126,15 @@ const Details = ({ onBack, id }) => (
-
+
Loading...
-
- {reviews => reviews.map(Review)}
+
+ {reviews => reviews.map(Review)}
)}
-
+
)
diff --git a/examples/with-abortcontroller/src/index.js b/examples/with-abortcontroller/src/index.js
index 6549999f..e9e0041e 100644
--- a/examples/with-abortcontroller/src/index.js
+++ b/examples/with-abortcontroller/src/index.js
@@ -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 ? cancel : start }
- {isLoading ? (
+ {isPending ? cancel : start }
+ {isPending ? (
Loading...
) : (
Inspect network traffic to see requests being canceled.
diff --git a/examples/with-nextjs/pages/index.js b/examples/with-nextjs/pages/index.js
index d2da631d..d49eaeee 100644
--- a/examples/with-nextjs/pages/index.js
+++ b/examples/with-nextjs/pages/index.js
@@ -20,10 +20,10 @@ class Hello extends React.Component {
const { userId = 1 } = url.query
return (
-
+
Loading...
-
-
+
+
{data => (
<>
@@ -43,7 +43,7 @@ class Hello extends React.Component {
>
)}
-
+
This data is initially loaded server-side, then client-side when navigating prev/next.
diff --git a/examples/with-react-router/js/Contributors.js b/examples/with-react-router/js/Contributors.js
index d939617f..9fa0dd94 100644
--- a/examples/with-react-router/js/Contributors.js
+++ b/examples/with-react-router/js/Contributors.js
@@ -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 (
@@ -12,4 +12,4 @@ const Contributors = ({ data, error, isLoading }) => {
)
}
-export default Contributors
\ No newline at end of file
+export default Contributors
diff --git a/examples/with-react-router/js/Repositories.js b/examples/with-react-router/js/Repositories.js
index 085737b7..898f769d 100644
--- a/examples/with-react-router/js/Repositories.js
+++ b/examples/with-react-router/js/Repositories.js
@@ -1,7 +1,7 @@
import React from "react"
-const Repositories = ({ data, error, isLoading }) => {
- if (isLoading) return "Loading Repositories..."
+const Repositories = ({ data, error, isPending }) => {
+ if (isPending) return "Loading Repositories..."
if (error) return "Error"
return (
@@ -12,4 +12,4 @@ const Repositories = ({ data, error, isLoading }) => {
)
}
-export default Repositories
\ No newline at end of file
+export default Repositories
diff --git a/package.json b/package.json
index cee8bf86..d6a2a89f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-async",
- "version": "5.1.2",
+ "version": "6.0.0-0",
"description": "React component for declarative promise resolution and data fetching",
"keywords": [
"react",
@@ -41,30 +41,30 @@
"prop-types": ">=15.5.7"
},
"devDependencies": {
- "@babel/plugin-proposal-object-rest-spread": "7.3.4",
- "@babel/plugin-transform-runtime": "7.2.0",
- "@babel/preset-env": "7.3.4",
+ "@babel/plugin-proposal-object-rest-spread": "7.4.0",
+ "@babel/plugin-transform-runtime": "7.4.0",
+ "@babel/preset-env": "7.4.2",
"@babel/preset-react": "7.0.0",
- "@pika/pack": "0.3.5",
- "@pika/plugin-build-node": "0.3.12",
- "@pika/plugin-build-types": "0.3.12",
- "@pika/plugin-build-web": "0.3.12",
- "@pika/plugin-standard-pkg": "0.3.12",
+ "@pika/pack": "0.3.6",
+ "@pika/plugin-build-node": "0.3.14",
+ "@pika/plugin-build-types": "0.3.14",
+ "@pika/plugin-build-web": "0.3.14",
+ "@pika/plugin-standard-pkg": "0.3.14",
"babel-eslint": "10.0.1",
- "babel-jest": "24.3.1",
- "eslint": "5.11.1",
- "eslint-config-prettier": "3.3.0",
- "eslint-plugin-jest": "22.3.0",
+ "babel-jest": "24.5.0",
+ "eslint": "5.15.3",
+ "eslint-config-prettier": "4.1.0",
+ "eslint-plugin-jest": "22.4.1",
"eslint-plugin-prettier": "3.0.1",
"eslint-plugin-promise": "4.0.1",
- "eslint-plugin-react": "7.12.0",
- "eslint-plugin-react-hooks": "1.0.1",
- "jest": "24.3.1",
- "jest-dom": "3.1.2",
- "prettier": "1.15.3",
+ "eslint-plugin-react": "7.12.4",
+ "eslint-plugin-react-hooks": "1.6.0",
+ "jest": "24.5.0",
+ "jest-dom": "3.1.3",
+ "prettier": "1.16.4",
"react": "16.8.1",
- "react-dom": "16.8.1",
- "react-testing-library": "5.5.3"
+ "react-dom": "16.8.5",
+ "react-testing-library": "6.0.3"
},
"jest": {
"collectCoverage": true,
@@ -76,6 +76,7 @@
"@pika/plugin-standard-pkg",
{
"exclude": [
+ "specs.js",
"*.spec.js"
]
}
diff --git a/src/Async.js b/src/Async.js
index f1319684..4ce6cd61 100644
--- a/src/Async.js
+++ b/src/Async.js
@@ -83,7 +83,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
this.abortController = new window.AbortController()
}
this.counter++
- this.dispatch({ type: actionTypes.start, meta: { counter: this.counter } })
+ this.mounted && this.dispatch({ type: actionTypes.start, meta: { counter: this.counter } })
}
load() {
@@ -118,7 +118,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
cancel() {
this.counter++
this.abortController.abort()
- this.dispatch({ type: actionTypes.cancel, meta: { counter: this.counter } })
+ this.mounted && this.dispatch({ type: actionTypes.cancel, meta: { counter: this.counter } })
}
onResolve(counter) {
@@ -179,16 +179,16 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
}
/**
- * Renders only when deferred promise is pending (not yet run).
+ * Renders only when no promise has started or completed yet.
*
* @prop {Function|Node} children Function (passing state) or React node
- * @prop {boolean} persist Show until we have data, even while loading or when an error occurred
+ * @prop {boolean} persist Show until we have data, even while pending (loading) or when an error occurred
*/
- const Pending = ({ children, persist }) => (
+ const Initial = ({ children, persist }) => (
{state => {
if (state.data !== undefined) return null
- if (!persist && state.isLoading) return null
+ if (!persist && state.isPending) return null
if (!persist && state.error !== undefined) return null
return isFunction(children) ? children(state) : children || null
}}
@@ -196,22 +196,22 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
)
if (PropTypes) {
- Pending.propTypes = {
+ Initial.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool,
}
}
/**
- * Renders only while loading.
+ * Renders only while pending (promise is loading).
*
* @prop {Function|Node} children Function (passing state) or React node
* @prop {boolean} initial Show only on initial load (data is undefined)
*/
- const Loading = ({ children, initial }) => (
+ const Pending = ({ children, initial }) => (
{state => {
- if (!state.isLoading) return null
+ if (!state.isPending) return null
if (initial && state.data !== undefined) return null
return isFunction(children) ? children(state) : children || null
}}
@@ -219,7 +219,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
)
if (PropTypes) {
- Loading.propTypes = {
+ Pending.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
initial: PropTypes.bool,
}
@@ -229,13 +229,13 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
* Renders only when promise is resolved.
*
* @prop {Function|Node} children Function (passing data and state) or React node
- * @prop {boolean} persist Show old data while loading
+ * @prop {boolean} persist Show old data while pending (promise is loading)
*/
- const Resolved = ({ children, persist }) => (
+ const Fulfilled = ({ children, persist }) => (
{state => {
if (state.data === undefined) return null
- if (!persist && state.isLoading) return null
+ if (!persist && state.isPending) return null
if (!persist && state.error !== undefined) return null
return isFunction(children) ? children(state.data, state) : children || null
}}
@@ -243,7 +243,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
)
if (PropTypes) {
- Resolved.propTypes = {
+ Fulfilled.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool,
}
@@ -253,13 +253,13 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
* Renders only when promise is rejected.
*
* @prop {Function|Node} children Function (passing error and state) or React node
- * @prop {boolean} persist Show old error while loading
+ * @prop {boolean} persist Show old error while pending (promise is loading)
*/
const Rejected = ({ children, persist }) => (
{state => {
if (state.error === undefined) return null
- if (state.isLoading && !persist) return null
+ if (state.isPending && !persist) return null
return isFunction(children) ? children(state.error, state) : children || null
}}
@@ -272,16 +272,43 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
}
}
- Async.Pending = Pending
- Async.Loading = Loading
- Async.Resolved = Resolved
- Async.Rejected = Rejected
+ /**
+ * Renders only when promise is fulfilled or rejected.
+ *
+ * @prop {Function|Node} children Function (passing state) or React node
+ * @prop {boolean} persist Show old data or error while pending (promise is loading)
+ */
+ const Settled = ({ children, persist }) => (
+
+ {state => {
+ if (state.isInitial) return null
+ if (state.isPending && !persist) return null
+ return isFunction(children) ? children(state) : children || null
+ }}
+
+ )
+
+ if (PropTypes) {
+ Settled.propTypes = {
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
+ persist: PropTypes.bool,
+ }
+ }
+
+ Initial.displayName = `${displayName}.Initial`
+ Pending.displayName = `${displayName}.Pending`
+ Fulfilled.displayName = `${displayName}.Fulfilled`
+ Rejected.displayName = `${displayName}.Rejected`
+ Settled.displayName = `${displayName}.Settled`
Async.displayName = displayName
- Async.Pending.displayName = `${displayName}.Pending`
- Async.Loading.displayName = `${displayName}.Loading`
- Async.Resolved.displayName = `${displayName}.Resolved`
- Async.Rejected.displayName = `${displayName}.Rejected`
+ Async.Initial = Initial
+ Async.Pending = Pending
+ Async.Loading = Pending // alias
+ Async.Fulfilled = Fulfilled
+ Async.Resolved = Fulfilled // alias
+ Async.Rejected = Rejected
+ Async.Settled = Settled
return Async
}
diff --git a/src/Async.spec.js b/src/Async.spec.js
index 942014e0..bf55060d 100644
--- a/src/Async.spec.js
+++ b/src/Async.spec.js
@@ -31,33 +31,35 @@ describe("Async", () => {
let two
const { rerender } = render(
-
+
{value => {
one = value
}}
-
+
)
rerender(
-
+
{value => {
two = value
}}
-
+
)
expect(one).toBe(two)
})
})
-describe("Async.Resolved", () => {
+describe("Async.Fulfilled", () => {
test("renders only after the promise is resolved", async () => {
const promiseFn = () => resolveTo("ok")
const deferFn = () => rejectTo("fail")
const { getByText, queryByText } = render(
- {(data, { run }) => {data} }
+
+ {(data, { run }) => {data} }
+
{error => error}
)
@@ -76,9 +78,9 @@ describe("Async.Resolved", () => {
const deferFn = () => rejectTo("fail")
const { getByText, queryByText } = render(
-
+
{(data, { run }) => {data} }
-
+
{error => error}
)
@@ -92,60 +94,60 @@ describe("Async.Resolved", () => {
expect(queryByText("fail")).toBeInTheDocument()
})
- test("Async.Resolved works also with nested Async", async () => {
+ test("Async.Fulfilled works also with nested Async", async () => {
const outer = () => resolveIn(0)("outer")
const inner = () => resolveIn(100)("inner")
const { getByText, queryByText } = render(
-
+
{outer => (
- {outer} loading
- {inner => outer + " " + inner}
+ {outer} pending
+ {inner => outer + " " + inner}
)}
-
+
)
- expect(queryByText("outer loading")).toBeNull()
- await waitForElement(() => getByText("outer loading"))
+ expect(queryByText("outer pending")).toBeNull()
+ await waitForElement(() => getByText("outer pending"))
expect(queryByText("outer inner")).toBeNull()
await waitForElement(() => getByText("outer inner"))
expect(queryByText("outer inner")).toBeInTheDocument()
})
})
-describe("Async.Loading", () => {
- test("renders only while the promise is loading", async () => {
+describe("Async.Pending", () => {
+ test("renders only while the promise is pending", async () => {
const promiseFn = () => resolveTo("ok")
const { getByText, queryByText } = render(
- loading
- done
+ pending
+ done
)
- expect(queryByText("loading")).toBeInTheDocument()
+ expect(queryByText("pending")).toBeInTheDocument()
await waitForElement(() => getByText("done"))
- expect(queryByText("loading")).toBeNull()
+ expect(queryByText("pending")).toBeNull()
})
})
-describe("Async.Pending", () => {
- test("renders only while the deferred promise is pending", async () => {
+describe("Async.Initial", () => {
+ test("renders only while the deferred promise has not started yet", async () => {
const deferFn = () => resolveTo("ok")
const { getByText, queryByText } = render(
- {({ run }) => pending }
- loading
- done
+ {({ run }) => initial }
+ pending
+ done
)
+ expect(queryByText("initial")).toBeInTheDocument()
+ fireEvent.click(getByText("initial"))
+ expect(queryByText("initial")).toBeNull()
expect(queryByText("pending")).toBeInTheDocument()
- fireEvent.click(getByText("pending"))
- expect(queryByText("pending")).toBeNull()
- expect(queryByText("loading")).toBeInTheDocument()
await waitForElement(() => getByText("done"))
- expect(queryByText("loading")).toBeNull()
+ expect(queryByText("pending")).toBeNull()
})
})
@@ -163,6 +165,32 @@ describe("Async.Rejected", () => {
})
})
+describe("Async.Settled", () => {
+ test("renders after the promise is fulfilled", async () => {
+ const promiseFn = () => resolveTo("value")
+ const { getByText, queryByText } = render(
+
+ {({ data }) => data}
+
+ )
+ expect(queryByText("value")).toBeNull()
+ await waitForElement(() => getByText("value"))
+ expect(queryByText("value")).toBeInTheDocument()
+ })
+
+ test("renders after the promise is rejected", async () => {
+ const promiseFn = () => rejectTo("err")
+ const { getByText, queryByText } = render(
+
+ {({ error }) => error}
+
+ )
+ expect(queryByText("err")).toBeNull()
+ await waitForElement(() => getByText("err"))
+ expect(queryByText("err")).toBeInTheDocument()
+ })
+})
+
describe("createInstance", () => {
test("allows setting default props", async () => {
const promiseFn = () => resolveTo("done")
@@ -190,11 +218,11 @@ describe("createInstance", () => {
const CustomAsync = createInstance({ promiseFn })
const { getByText } = render(
- loading
- resolved
+ pending
+ resolved
)
- await waitForElement(() => getByText("loading"))
+ await waitForElement(() => getByText("pending"))
await waitForElement(() => getByText("resolved"))
})
diff --git a/src/index.d.ts b/src/index.d.ts
index 42167c82..04e4ccb1 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -20,13 +20,8 @@ export interface AsyncProps extends AsyncOptions {
children?: AsyncChildren
}
-export interface AsyncState {
- data?: T
- error?: Error
+interface AbstractState {
initialValue?: T
- isLoading: boolean
- startedAt?: Date
- finishedAt?: Date
counter: number
cancel: () => void
run: (...args: any[]) => Promise
@@ -35,17 +30,83 @@ export interface AsyncState {
setError: (error: Error, callback?: () => void) => Error
}
+export type AsyncInitial = AbstractState & {
+ data: undefined
+ error: undefined
+ startedAt: undefined
+ finishedAt: undefined
+ status: "initial"
+ isInitial: false
+ isPending: false
+ isLoading: false
+ isFulfilled: false
+ isResolved: false
+ isRejected: false
+ isSettled: false
+}
+export type AsyncPending = AbstractState & {
+ data: T | undefined
+ error: Error | undefined
+ startedAt: Date
+ finishedAt: undefined
+ status: "pending"
+ isInitial: false
+ isPending: true
+ isLoading: true
+ isFulfilled: false
+ isResolved: false
+ isRejected: false
+ isSettled: false
+}
+export type AsyncFulfilled = AbstractState & {
+ data: T
+ error: undefined
+ startedAt: Date
+ finishedAt: Date
+ status: "fulfilled"
+ isInitial: false
+ isPending: false
+ isLoading: false
+ isFulfilled: true
+ isResolved: true
+ isRejected: false
+ isSettled: true
+}
+export type AsyncRejected = AbstractState & {
+ data: T | undefined
+ error: Error
+ startedAt: Date
+ finishedAt: Date
+ status: "rejected"
+ isInitial: false
+ isPending: false
+ isLoading: false
+ isFulfilled: false
+ isResolved: false
+ isRejected: true
+ isSettled: true
+}
+export type AsyncState = AsyncInitial | AsyncPending | AsyncFulfilled | AsyncRejected
+
declare class Async extends React.Component, AsyncState> {}
declare namespace Async {
- export function Pending(props: {
+ export function Initial(props: {
children?: AsyncChildren
persist?: boolean
}): React.ReactNode
+ export function Pending(props: {
+ children?: AsyncChildren
+ initial?: boolean
+ }): React.ReactNode
export function Loading(props: {
children?: AsyncChildren
initial?: boolean
}): React.ReactNode
+ export function Fulfilled(props: {
+ children?: AsyncChildren
+ persist?: boolean
+ }): React.ReactNode
export function Resolved(props: {
children?: AsyncChildren
persist?: boolean
@@ -54,6 +115,10 @@ declare namespace Async {
children?: AsyncChildren
persist?: boolean
}): React.ReactNode
+ export function Settled(props: {
+ children?: AsyncChildren
+ persist?: boolean
+ }): React.ReactNode
}
declare function createInstance(defaultProps?: AsyncProps): Async
diff --git a/src/index.js b/src/index.js
index e6c19e75..3fa43061 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,3 +2,4 @@ import Async from "./Async"
export { createInstance } from "./Async"
export { default as useAsync, useFetch } from "./useAsync"
export default Async
+export { statusTypes } from "./status"
diff --git a/src/reducer.js b/src/reducer.js
index 9a4d52fd..3c6b0050 100644
--- a/src/reducer.js
+++ b/src/reducer.js
@@ -1,3 +1,5 @@
+import { getInitialStatus, getIdleStatus, getStatusProps, statusTypes } from "./status"
+
export const actionTypes = {
start: "start",
cancel: "cancel",
@@ -9,9 +11,9 @@ export const init = ({ initialValue, promise, promiseFn }) => ({
initialValue,
data: initialValue instanceof Error ? undefined : initialValue,
error: initialValue instanceof Error ? initialValue : undefined,
- isLoading: !!promise || (promiseFn && !initialValue),
startedAt: promise || promiseFn ? new Date() : undefined,
finishedAt: initialValue ? new Date() : undefined,
+ ...getStatusProps(getInitialStatus(initialValue, promise || promiseFn)),
counter: 0,
})
@@ -20,16 +22,17 @@ export const reducer = (state, { type, payload, meta }) => {
case actionTypes.start:
return {
...state,
- isLoading: true,
startedAt: new Date(),
finishedAt: undefined,
+ ...getStatusProps(statusTypes.pending),
counter: meta.counter,
}
case actionTypes.cancel:
return {
...state,
- isLoading: false,
startedAt: undefined,
+ finishedAt: undefined,
+ ...getStatusProps(getIdleStatus(state.error || state.data)),
counter: meta.counter,
}
case actionTypes.fulfill:
@@ -37,15 +40,15 @@ export const reducer = (state, { type, payload, meta }) => {
...state,
data: payload,
error: undefined,
- isLoading: false,
finishedAt: new Date(),
+ ...getStatusProps(statusTypes.fulfilled),
}
case actionTypes.reject:
return {
...state,
error: payload,
- isLoading: false,
finishedAt: new Date(),
+ ...getStatusProps(statusTypes.rejected),
}
}
}
diff --git a/src/specs.js b/src/specs.js
index d6af564e..b8aefb1d 100644
--- a/src/specs.js
+++ b/src/specs.js
@@ -17,9 +17,14 @@ export const common = Async => () => {
expect(renderProps).toHaveProperty("data")
expect(renderProps).toHaveProperty("error")
expect(renderProps).toHaveProperty("initialValue")
- expect(renderProps).toHaveProperty("isLoading")
expect(renderProps).toHaveProperty("startedAt")
expect(renderProps).toHaveProperty("finishedAt")
+ expect(renderProps).toHaveProperty("isInitial")
+ expect(renderProps).toHaveProperty("isPending")
+ expect(renderProps).toHaveProperty("isLoading")
+ expect(renderProps).toHaveProperty("isFulfilled")
+ expect(renderProps).toHaveProperty("isRejected")
+ expect(renderProps).toHaveProperty("isSettled")
expect(renderProps).toHaveProperty("counter")
expect(renderProps).toHaveProperty("cancel")
expect(renderProps).toHaveProperty("run")
@@ -310,8 +315,8 @@ export const withPromiseFn = (Async, abortCtrl) => () => {
const states = []
const { getByText } = render(
- {({ data, isLoading }) => {
- states.push(isLoading)
+ {({ data, isPending }) => {
+ states.push(isPending)
return data || null
}}
diff --git a/src/status.js b/src/status.js
new file mode 100644
index 00000000..0af8fd52
--- /dev/null
+++ b/src/status.js
@@ -0,0 +1,30 @@
+export const statusTypes = {
+ initial: "initial",
+ pending: "pending",
+ fulfilled: "fulfilled",
+ rejected: "rejected",
+}
+
+export const getInitialStatus = (value, promise) => {
+ if (value instanceof Error) return statusTypes.rejected
+ if (value !== undefined) return statusTypes.fulfilled
+ if (promise) return statusTypes.pending
+ return statusTypes.initial
+}
+
+export const getIdleStatus = value => {
+ if (value instanceof Error) return statusTypes.rejected
+ if (value !== undefined) return statusTypes.fulfilled
+ return statusTypes.initial
+}
+
+export const getStatusProps = status => ({
+ status,
+ isInitial: status === statusTypes.initial,
+ isPending: status === statusTypes.pending,
+ isLoading: status === statusTypes.pending, // alias
+ isFulfilled: status === statusTypes.fulfilled,
+ isResolved: status === statusTypes.fulfilled, // alias
+ isRejected: status === statusTypes.rejected,
+ isSettled: status === statusTypes.fulfilled || status === statusTypes.rejected,
+})
diff --git a/src/status.spec.js b/src/status.spec.js
new file mode 100644
index 00000000..5f2fa580
--- /dev/null
+++ b/src/status.spec.js
@@ -0,0 +1,32 @@
+/* eslint-disable react/prop-types */
+
+import "jest-dom/extend-expect"
+
+import { getInitialStatus, getIdleStatus, statusTypes } from "./status"
+
+describe("getInitialStatus", () => {
+ test("returns 'initial' when given an undefined value", () => {
+ expect(getInitialStatus(undefined)).toEqual(statusTypes.initial)
+ })
+ test("returns 'pending' when given only a promise", () => {
+ expect(getInitialStatus(undefined, Promise.resolve("foo"))).toEqual(statusTypes.pending)
+ })
+ test("returns 'rejected' when given an Error value", () => {
+ expect(getInitialStatus(new Error("oops"))).toEqual(statusTypes.rejected)
+ })
+ test("returns 'fulfilled' when given any other value", () => {
+ expect(getInitialStatus(null)).toEqual(statusTypes.fulfilled)
+ })
+})
+
+describe("getIdleStatus", () => {
+ test("returns 'initial' when given an undefined value", () => {
+ expect(getIdleStatus(undefined)).toEqual(statusTypes.initial)
+ })
+ test("returns 'rejected' when given an Error value", () => {
+ expect(getIdleStatus(new Error("oops"))).toEqual(statusTypes.rejected)
+ })
+ test("returns 'fulfilled' when given any other value", () => {
+ expect(getIdleStatus(null)).toEqual(statusTypes.fulfilled)
+ })
+})
diff --git a/src/useAsync.js b/src/useAsync.js
index 14289f99..9f9fc393 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -42,7 +42,7 @@ const useAsync = (arg1, arg2) => {
abortController.current = new window.AbortController()
}
counter.current++
- dispatch({ type: actionTypes.start, meta: { counter: counter.current } })
+ isMounted.current && dispatch({ type: actionTypes.start, meta: { counter: counter.current } })
}
const load = () => {
@@ -75,34 +75,27 @@ const useAsync = (arg1, arg2) => {
const cancel = () => {
counter.current++
abortController.current.abort()
- dispatch({ type: actionTypes.cancel, meta: { counter: counter.current } })
+ isMounted.current && dispatch({ type: actionTypes.cancel, meta: { counter: counter.current } })
}
useEffect(() => {
if (watchFn && prevOptions.current && watchFn(options, prevOptions.current)) load()
})
- useEffect(
- () => {
- promise || promiseFn ? load() : cancel()
- },
- [promise, promiseFn, watch]
- )
+ useEffect(() => {
+ promise || promiseFn ? load() : cancel()
+ }, [promise, promiseFn, watch])
useEffect(() => () => (isMounted.current = false), [])
useEffect(() => () => abortController.current.abort(), [])
useEffect(() => (prevOptions.current = options) && undefined)
- useDebugValue(state, ({ startedAt, finishedAt, error }) => {
- if (startedAt && (!finishedAt || finishedAt < startedAt)) return `[${counter.current}] Loading`
- if (finishedAt) return error ? `[${counter.current}] Rejected` : `[${counter.current}] Resolved`
- return `[${counter.current}] Pending`
- })
+ useDebugValue(state, ({ status }) => `[${counter.current}] ${status}`)
return useMemo(
() => ({
...state,
+ cancel,
run,
reload: () => (lastArgs.current ? run(...lastArgs.current) : load()),
- cancel,
setData,
setError,
}),
@@ -131,11 +124,7 @@ const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => {
[JSON.stringify(input), JSON.stringify(init)]
),
})
- useDebugValue(state, ({ startedAt, finishedAt, error, counter }) => {
- if (startedAt && (!finishedAt || finishedAt < startedAt)) return `[${counter}] Loading`
- if (finishedAt) return error ? `[${counter}] Rejected` : `[${counter}] Resolved`
- return `[${counter}] Pending`
- })
+ useDebugValue(state, ({ counter, status }) => `[${counter}] ${status}`)
return state
}