From cf5ff977eafe0ac80997eb648f1c0e7cee362b6e Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 10:42:19 +0100
Subject: [PATCH 01/18] Add useAsync.
---
src/index.js | 1 +
src/useAsync.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 src/useAsync.js
diff --git a/src/index.js b/src/index.js
index 68811457..ee4f0bda 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,5 @@
import React from "react"
+export { default as useAsync } from "./useAsync"
const isFunction = arg => typeof arg === "function"
diff --git a/src/useAsync.js b/src/useAsync.js
new file mode 100644
index 00000000..8956184e
--- /dev/null
+++ b/src/useAsync.js
@@ -0,0 +1,96 @@
+import { useState, useEffect, useMemo } from "react"
+
+const useAsync = props => {
+ let counter = 0
+ let isMounted = false
+ let lastArgs = undefined
+
+ const initialError = props.initialValue instanceof Error ? props.initialValue : undefined
+ const initialData = initialError ? undefined : props.initialValue
+ const [data, setData] = useState(initialData)
+ const [error, setError] = useState(initialError)
+ const [startedAt, setStartedAt] = useState(props.promiseFn ? new Date() : undefined)
+ const [finishedAt, setFinishedAt] = useState(props.initialValue ? new Date() : undefined)
+
+ const cancel = () => {
+ counter++
+ setStartedAt(undefined)
+ }
+
+ const start = () => {
+ counter++
+ setStartedAt(new Date())
+ setFinishedAt(undefined)
+ }
+
+ const end = () => setFinishedAt(new Date())
+
+ const handleData = (data, callback = () => {}) => {
+ if (isMounted) {
+ end()
+ setData(data)
+ setError(undefined)
+ callback(data)
+ }
+ return data
+ }
+
+ const handleError = (error, callback = () => {}) => {
+ if (isMounted) {
+ end()
+ setError(error)
+ callback(error)
+ }
+ return error
+ }
+
+ const onResolve = count => data => count === counter && handleData(data, props.onResolve)
+ const onReject = count => error => count === counter && handleError(error, props.onReject)
+
+ const load = () => {
+ if (props.promiseFn) {
+ start()
+ props.promiseFn(props).then(onResolve(counter), onReject(counter))
+ }
+ }
+
+ const run = (...args) => {
+ if (props.deferFn) {
+ lastArgs = args
+ start()
+ return props.deferFn(...args, props).then(onResolve(counter), onReject(counter))
+ }
+ }
+
+ const reload = () => (lastArgs ? run(...lastArgs) : load())
+
+ useEffect(() => {
+ isMounted = true
+ return () => (isMounted = false)
+ }, [])
+
+ useEffect(load, [props.promiseFn, props.watch])
+
+ return useMemo(
+ () => ({
+ isLoading: startedAt && (!finishedAt || finishedAt < startedAt),
+ startedAt,
+ finishedAt,
+ data,
+ error,
+ cancel,
+ run,
+ reload,
+ setData: handleData,
+ setError: handleError,
+ initialValue: props.initialValue
+ }),
+ [data, error, startedAt, finishedAt]
+ )
+}
+
+const unsupported = () => {
+ throw new Error("useAsync requires react@16.7.0 or later")
+}
+
+export default (useState ? useAsync : unsupported)
From 4168f2bf7909f61a0e480fd45120d1bd51f090b8 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 10:43:09 +0100
Subject: [PATCH 02/18] Don't include spec files in build.
---
package.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index e4c67255..50700faa 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"src"
],
"scripts": {
- "build": "babel src -d lib",
+ "build": "rimraf lib && babel src -d lib --ignore '**/*spec.js'",
"test": "jest src",
"test:watch": "npm run test -- --watch",
"test:compat": "npm run test:backwards && npm run test:forwards && npm run test:latest",
@@ -47,7 +47,8 @@
"jest-dom": "2.1.0",
"react": "16.7.0-alpha.0",
"react-dom": "16.7.0-alpha.0",
- "react-testing-library": "5.2.3"
+ "react-testing-library": "5.2.3",
+ "rimraf": "2.6.2"
},
"jest": {
"coverageDirectory": "./coverage/",
From edb89139ae50a763a24d9c2056c9c3891fb2b452 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 11:30:22 +0100
Subject: [PATCH 03/18] Add useAsync to README.
---
README.md | 30 ++++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index fafa994c..193160a7 100644
--- a/README.md
+++ b/README.md
@@ -31,13 +31,13 @@
-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.
+React component 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 any (native) promise
-- Choose between Render Props and Context-based helper components
+- Choose between Render Props, Context-based helper components or the `useAsync` hook
- Provides convenient `isLoading`, `startedAt` and `finishedAt` metadata
- Provides `cancel` and `reload` actions
- Automatic re-run using `watch` prop
@@ -71,6 +71,28 @@ npm install --save react-async
## Usage
+As a hook with `useAsync`:
+
+```js
+import { useAsync } from "react-async"
+
+const loadJson = () => fetch("/some/url").then(res => res.json())
+
+const MyComponent = () => {
+ const { data, error, isLoading } = useAsync({ promiseFn: loadJson })
+ if (isLoading) return "Loading..."
+ if (error) return `Something went wrong: ${error.message}`
+ if (data)
+ return (
+
+
Loaded some data:
+
{JSON.stringify(data, null, 2)}
+
+ )
+ return null
+}
+```
+
Using render props for ultimate flexibility:
```js
From 0ba881349cc8ff742a9a896c6948c79240250017 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 12:00:13 +0100
Subject: [PATCH 04/18] Nicer way to pass a value to the setTimeout callback.
---
src/spec.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/spec.js b/src/spec.js
index 798714f4..861f6c26 100644
--- a/src/spec.js
+++ b/src/spec.js
@@ -5,9 +5,9 @@ import Async, { createInstance } from "./"
afterEach(cleanup)
-const resolveIn = ms => value => new Promise(resolve => setTimeout(() => resolve(value), ms))
+const resolveIn = ms => value => new Promise(resolve => setTimeout(resolve, ms, value))
const resolveTo = resolveIn(0)
-const rejectIn = ms => err => new Promise((resolve, reject) => setTimeout(() => reject(err), ms))
+const rejectIn = ms => err => new Promise((resolve, reject) => setTimeout(reject, ms, err))
const rejectTo = rejectIn(0)
test("runs promiseFn on mount", () => {
From 3c6a75b32bde3d886053e9cf6e31dd803051365c Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 12:00:52 +0100
Subject: [PATCH 05/18] Add first (failing) test for useState.
---
src/spec.js | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/spec.js b/src/spec.js
index 861f6c26..81a12427 100644
--- a/src/spec.js
+++ b/src/spec.js
@@ -1,7 +1,7 @@
import "jest-dom/extend-expect"
import React from "react"
import { render, fireEvent, cleanup, waitForElement } from "react-testing-library"
-import Async, { createInstance } from "./"
+import Async, { createInstance, useAsync } from "./"
afterEach(cleanup)
@@ -423,3 +423,10 @@ test("an unrelated change in props does not update the Context", async () => {
)
expect(one).toBe(two)
})
+
+test("useAsync returns render props", async () => {
+ const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
+ const Async = ({ children, ...props }) => children(useAsync(props))
+ const { getByText } = render({({ data }) => data || null} )
+ await waitForElement(() => getByText("done"))
+})
From 4e4d1d74b788873f750c76d266a7681316079358 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Mon, 29 Oct 2018 13:05:50 +0100
Subject: [PATCH 06/18] Add support for shorthand useState.
---
README.md | 17 +++++++++++++++++
src/useAsync.js | 33 +++++++++++++++++----------------
2 files changed, 34 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 193160a7..fb38b7c4 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,15 @@ const MyComponent = () => {
}
```
+Or using the shorthand version:
+
+```js
+const MyComponent = () => {
+ const { data, error, isLoading } = useAsync(loadJson)
+ // ...
+}
+```
+
Using render props for ultimate flexibility:
```js
@@ -194,6 +203,14 @@ Similarly, this allows you to set default `onResolve` and `onReject` callbacks.
- `setData` {Function} sets `data` to the passed value, unsets `error` and cancels any pending promise
- `setError` {Function} sets `error` to the passed value and cancels any pending promise
+### `useState`
+
+The `useState` hook accepts an object with the same props as ``. Alternatively you can use the shorthand syntax:
+
+```js
+useState(promiseFn, initialValue)
+```
+
## Examples
### Basic data fetching with loading indicator, error state and retry
diff --git a/src/useAsync.js b/src/useAsync.js
index 8956184e..5810aa70 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -1,16 +1,17 @@
import { useState, useEffect, useMemo } from "react"
-const useAsync = props => {
+const useAsync = (opts, init) => {
let counter = 0
let isMounted = false
let lastArgs = undefined
- const initialError = props.initialValue instanceof Error ? props.initialValue : undefined
- const initialData = initialError ? undefined : props.initialValue
- const [data, setData] = useState(initialData)
- const [error, setError] = useState(initialError)
- const [startedAt, setStartedAt] = useState(props.promiseFn ? new Date() : undefined)
- const [finishedAt, setFinishedAt] = useState(props.initialValue ? new Date() : undefined)
+ const options = typeof opts === "function" ? { promiseFn: opts, initialValue: init } : opts
+ const { promiseFn, deferFn, initialValue, onResolve, onReject, watch } = options
+
+ const [data, setData] = useState(initialValue instanceof Error ? undefined : initialValue)
+ const [error, setError] = useState(initialValue instanceof Error ? initialValue : undefined)
+ const [startedAt, setStartedAt] = useState(promiseFn ? new Date() : undefined)
+ const [finishedAt, setFinishedAt] = useState(initialValue ? new Date() : undefined)
const cancel = () => {
counter++
@@ -44,21 +45,21 @@ const useAsync = props => {
return error
}
- const onResolve = count => data => count === counter && handleData(data, props.onResolve)
- const onReject = count => error => count === counter && handleError(error, props.onReject)
+ const handleResolve = count => data => count === counter && handleData(data, onResolve)
+ const handleReject = count => error => count === counter && handleError(error, onReject)
const load = () => {
- if (props.promiseFn) {
+ if (promiseFn) {
start()
- props.promiseFn(props).then(onResolve(counter), onReject(counter))
+ promiseFn(options).then(handleResolve(counter), handleReject(counter))
}
}
const run = (...args) => {
- if (props.deferFn) {
+ if (deferFn) {
lastArgs = args
start()
- return props.deferFn(...args, props).then(onResolve(counter), onReject(counter))
+ return deferFn(...args, options).then(handleResolve(counter), handleReject(counter))
}
}
@@ -69,7 +70,7 @@ const useAsync = props => {
return () => (isMounted = false)
}, [])
- useEffect(load, [props.promiseFn, props.watch])
+ useEffect(load, [promiseFn, watch])
return useMemo(
() => ({
@@ -78,12 +79,12 @@ const useAsync = props => {
finishedAt,
data,
error,
+ initialValue,
cancel,
run,
reload,
setData: handleData,
- setError: handleError,
- initialValue: props.initialValue
+ setError: handleError
}),
[data, error, startedAt, finishedAt]
)
From d8a82451c2f99759140f79a374f65ad1ae8ae0d1 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Wed, 7 Nov 2018 00:46:12 +0100
Subject: [PATCH 07/18] Update useAsync to use useRef for instance variables.
---
package-lock.json | 22 ++--
src/spec.js | 9 +-
src/useAsync.js | 89 ++++++-------
src/useAsync.spec.js | 305 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 356 insertions(+), 69 deletions(-)
create mode 100644 src/useAsync.spec.js
diff --git a/package-lock.json b/package-lock.json
index 797f6d57..b7714b30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -90,9 +90,9 @@
},
"dependencies": {
"acorn": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz",
- "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==",
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz",
+ "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==",
"dev": true
}
}
@@ -117,7 +117,7 @@
},
"ansi-escapes": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
"integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
"dev": true
},
@@ -6325,9 +6325,9 @@
}
},
"spdx-license-ids": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz",
- "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz",
+ "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==",
"dev": true
},
"split-string": {
@@ -6346,9 +6346,9 @@
"dev": true
},
"sshpk": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.1.tgz",
- "integrity": "sha512-mSdgNUaidk+dRU5MhYtN9zebdzF2iG0cNPWy8HG+W8y+fT1JnSkh0fzzpjOa0L7P8i1Rscz38t0h4gPcKz43xA==",
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
+ "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
"dev": true,
"requires": {
"asn1": "~0.2.3",
@@ -6478,7 +6478,7 @@
},
"strip-eof": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
diff --git a/src/spec.js b/src/spec.js
index 81a12427..861f6c26 100644
--- a/src/spec.js
+++ b/src/spec.js
@@ -1,7 +1,7 @@
import "jest-dom/extend-expect"
import React from "react"
import { render, fireEvent, cleanup, waitForElement } from "react-testing-library"
-import Async, { createInstance, useAsync } from "./"
+import Async, { createInstance } from "./"
afterEach(cleanup)
@@ -423,10 +423,3 @@ test("an unrelated change in props does not update the Context", async () => {
)
expect(one).toBe(two)
})
-
-test("useAsync returns render props", async () => {
- const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
- const Async = ({ children, ...props }) => children(useAsync(props))
- const { getByText } = render({({ data }) => data || null} )
- await waitForElement(() => getByText("done"))
-})
diff --git a/src/useAsync.js b/src/useAsync.js
index 5810aa70..675429f2 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -1,92 +1,81 @@
-import { useState, useEffect, useMemo } from "react"
+import { useState, useEffect, useMemo, useRef } from "react"
const useAsync = (opts, init) => {
- let counter = 0
- let isMounted = false
- let lastArgs = undefined
+ const counter = useRef(0)
+ const isMounted = useRef(true)
+ const lastArgs = useRef(undefined)
const options = typeof opts === "function" ? { promiseFn: opts, initialValue: init } : opts
const { promiseFn, deferFn, initialValue, onResolve, onReject, watch } = options
- const [data, setData] = useState(initialValue instanceof Error ? undefined : initialValue)
- const [error, setError] = useState(initialValue instanceof Error ? initialValue : undefined)
- const [startedAt, setStartedAt] = useState(promiseFn ? new Date() : undefined)
- const [finishedAt, setFinishedAt] = useState(initialValue ? new Date() : undefined)
-
- const cancel = () => {
- counter++
- setStartedAt(undefined)
- }
-
- const start = () => {
- counter++
- setStartedAt(new Date())
- setFinishedAt(undefined)
- }
-
- const end = () => setFinishedAt(new Date())
+ const [state, setState] = useState({
+ data: initialValue instanceof Error ? undefined : initialValue,
+ error: initialValue instanceof Error ? initialValue : undefined,
+ startedAt: promiseFn ? new Date() : undefined,
+ finishedAt: initialValue ? new Date() : undefined,
+ })
const handleData = (data, callback = () => {}) => {
- if (isMounted) {
- end()
- setData(data)
- setError(undefined)
+ if (isMounted.current) {
+ setState(state => ({ ...state, data, error: undefined, finishedAt: new Date() }))
callback(data)
}
return data
}
const handleError = (error, callback = () => {}) => {
- if (isMounted) {
- end()
- setError(error)
+ if (isMounted.current) {
+ setState(state => ({ ...state, error, finishedAt: new Date() }))
callback(error)
}
return error
}
- const handleResolve = count => data => count === counter && handleData(data, onResolve)
- const handleReject = count => error => count === counter && handleError(error, onReject)
+ const handleResolve = count => data => count === counter.current && handleData(data, onResolve)
+ const handleReject = count => error => count === counter.current && handleError(error, onReject)
+
+ const start = () => {
+ counter.current++
+ setState(state => ({
+ ...state,
+ startedAt: new Date(),
+ finishedAt: undefined,
+ }))
+ }
const load = () => {
- if (promiseFn) {
+ if (promiseFn && !(initialValue && counter.current === 0)) {
start()
- promiseFn(options).then(handleResolve(counter), handleReject(counter))
+ promiseFn(options).then(handleResolve(counter.current), handleReject(counter.current))
}
}
const run = (...args) => {
if (deferFn) {
- lastArgs = args
start()
- return deferFn(...args, options).then(handleResolve(counter), handleReject(counter))
+ lastArgs.current = args
+ return deferFn(...args, options).then(handleResolve(counter.current), handleReject(counter.current))
}
}
- const reload = () => (lastArgs ? run(...lastArgs) : load())
-
- useEffect(() => {
- isMounted = true
- return () => (isMounted = false)
- }, [])
-
useEffect(load, [promiseFn, watch])
+ useEffect(() => () => (isMounted.current = false), [])
return useMemo(
() => ({
- isLoading: startedAt && (!finishedAt || finishedAt < startedAt),
- startedAt,
- finishedAt,
- data,
- error,
+ ...state,
+ isLoading: state.startedAt && (!state.finishedAt || state.finishedAt < state.startedAt),
initialValue,
- cancel,
run,
- reload,
+ reload: () => (lastArgs ? run(...lastArgs) : load()),
+ cancel: () => {
+ counter.current++
+ setState(state => ({ ...state, startedAt: undefined }))
+ },
setData: handleData,
- setError: handleError
+ setError: handleError,
}),
- [data, error, startedAt, finishedAt]
+ [state]
)
}
diff --git a/src/useAsync.spec.js b/src/useAsync.spec.js
new file mode 100644
index 00000000..693b3a74
--- /dev/null
+++ b/src/useAsync.spec.js
@@ -0,0 +1,305 @@
+import "jest-dom/extend-expect"
+import React from "react"
+import { render, fireEvent, cleanup, waitForElement } from "react-testing-library"
+import { useAsync } from "."
+
+afterEach(cleanup)
+
+const resolveIn = ms => value => new Promise(resolve => setTimeout(resolve, ms, value))
+const resolveTo = resolveIn(0)
+
+const Async = ({ children = () => null, ...props }) => children(useAsync(props))
+
+test("useAsync returns render props", async () => {
+ const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
+ const component = {({ data }) => data || null}
+ const { getByText, rerender } = render(component)
+ rerender(component) // needed to trigger useEffect
+ await waitForElement(() => getByText("done"))
+})
+
+test("useAsync passes rejection error to children as render prop", async () => {
+ const promiseFn = () => Promise.reject("oops")
+ const component = {({ error }) => error || null}
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("oops"))
+})
+
+test("useAsync passes isLoading boolean while the promise is running", async () => {
+ const promiseFn = () => resolveTo("done")
+ const states = []
+ const component = (
+
+ {({ data, isLoading }) => {
+ states.push(isLoading)
+ return data || null
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("done"))
+ expect(states).toEqual([true, true, false])
+})
+
+test("useAsync passes startedAt date when the promise starts", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ startedAt }) => {
+ if (startedAt) {
+ expect(startedAt.getTime()).toBeCloseTo(new Date().getTime(), -2)
+ return "started"
+ }
+ return null
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("started"))
+})
+
+test("useAsync passes finishedAt date when the promise finishes", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ data, finishedAt }) => {
+ if (finishedAt) {
+ expect(finishedAt.getTime()).toBeCloseTo(new Date().getTime(), -1)
+ return data || null
+ }
+ return null
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("done"))
+})
+
+test("useAsync passes reload function that re-runs the promise", () => {
+ const promiseFn = jest.fn().mockReturnValue(resolveTo())
+ const component = (
+
+ {({ reload }) => {
+ return reload
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ expect(promiseFn).toHaveBeenCalledTimes(1)
+ fireEvent.click(getByText("reload"))
+ expect(promiseFn).toHaveBeenCalledTimes(2)
+})
+
+test("useAsync re-runs the promise when the value of 'watch' changes", () => {
+ class Counter extends React.Component {
+ state = { count: 0 }
+ inc = () => this.setState(state => ({ count: state.count + 1 }))
+ render() {
+ return (
+
+ increment
+ {this.props.children(this.state.count)}
+
+ )
+ }
+ }
+ const promiseFn = jest.fn().mockReturnValue(resolveTo())
+ const component = {count => }
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ expect(promiseFn).toHaveBeenCalledTimes(1)
+ fireEvent.click(getByText("increment"))
+ rerender(component)
+ expect(promiseFn).toHaveBeenCalledTimes(2)
+ fireEvent.click(getByText("increment"))
+ rerender(component)
+ expect(promiseFn).toHaveBeenCalledTimes(3)
+})
+
+test("useAsync runs deferFn only when explicitly invoked, passing arguments and props", () => {
+ let counter = 1
+ const deferFn = jest.fn().mockReturnValue(resolveTo())
+ const component = (
+
+ {({ run }) => {
+ return run("go", counter++)}>run
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ expect(deferFn).not.toHaveBeenCalled()
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn, foo: "bar" }))
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn, foo: "bar" }))
+})
+
+test("useAsync reload uses the arguments of the previous run", () => {
+ let counter = 1
+ const deferFn = jest.fn().mockReturnValue(resolveTo())
+ const component = (
+
+ {({ run, reload }) => {
+ return (
+
+ run("go", counter++)}>run
+ reload
+
+ )
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ expect(deferFn).not.toHaveBeenCalled()
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }))
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
+ fireEvent.click(getByText("reload"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
+})
+
+test("useAsync only accepts the last invocation of the promise", async () => {
+ let i = 0
+ const resolves = [resolveIn(10)("a"), resolveIn(20)("b"), resolveIn(10)("c")]
+ const component = (
+ resolves[i]}>
+ {({ data, run }) => {
+ if (data) {
+ expect(data).toBe("c")
+ return "done"
+ }
+ return run(i)}>run
+ }}
+
+ )
+ const { getByText } = render(component)
+ fireEvent.click(getByText("run"))
+ i++
+ fireEvent.click(getByText("run"))
+ i++
+ fireEvent.click(getByText("run"))
+ await waitForElement(() => getByText("done"))
+})
+
+test("useAsync invokes onResolve callback when promise resolves", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component =
+ const { rerender } = render(component)
+ rerender(component)
+ await Promise.resolve()
+ expect(onResolve).toHaveBeenCalledWith("ok")
+})
+
+test("useAsync invokes onReject callback when promise rejects", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.reject("err"))
+ const onReject = jest.fn()
+ const component =
+ const { rerender } = render(component)
+ rerender(component)
+ await Promise.resolve()
+ expect(onReject).toHaveBeenCalledWith("err")
+})
+
+test("useAsync cancels pending promise when unmounted", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component =
+ const { unmount, rerender } = render(component)
+ rerender(component)
+ unmount()
+ await Promise.resolve()
+ expect(onResolve).not.toHaveBeenCalled()
+})
+
+test("useAsync cancels and restarts the promise when promiseFn changes", async () => {
+ const promiseFn1 = jest.fn().mockReturnValue(resolveIn(10)("one"))
+ const promiseFn2 = jest.fn().mockReturnValue(resolveIn(10)("two"))
+ const onResolve = jest.fn()
+ const component1 =
+ const component2 =
+ const { rerender } = render(component1)
+ rerender(component2)
+ await resolveIn(10)()
+ rerender(component2)
+ expect(onResolve).not.toHaveBeenCalledWith("one")
+ expect(onResolve).toHaveBeenCalledWith("two")
+})
+
+test("useAsync does not run promiseFn on mount when initialValue is provided", () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
+ const component =
+ const { rerender } = render(component)
+ rerender(component)
+ expect(promiseFn).not.toHaveBeenCalled()
+})
+
+test("useAsync does not start loading when using initialValue", async () => {
+ const promiseFn = () => resolveTo("done")
+ const states = []
+ const component = (
+
+ {({ data, isLoading }) => {
+ states.push(isLoading)
+ return data
+ }}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("done"))
+ expect(states).toEqual([false])
+})
+
+test("useAsync passes initialValue to children immediately", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ data }) => data}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("done"))
+})
+
+test("useAsync sets error instead of data when initialValue is an Error object", async () => {
+ const promiseFn = () => resolveTo("done")
+ const error = new Error("oops")
+ const component = (
+
+ {({ error }) => error.message}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("oops"))
+})
+
+test("useAsync can be nested", async () => {
+ const outerFn = () => resolveIn(0)("outer")
+ const innerFn = () => resolveIn(100)("inner")
+ const component = (
+
+ {({ data: outer }) => (
+
+ {({ data: inner }) => {
+ return outer + " " + inner
+ }}
+
+ )}
+
+ )
+ const { getByText, rerender } = render(component)
+ rerender(component)
+ await waitForElement(() => getByText("outer undefined"))
+ await waitForElement(() => getByText("outer inner"))
+})
From 246b41a8974b2b00b42019b072f9598d5c3ec3ad Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Wed, 7 Nov 2018 11:12:09 +0100
Subject: [PATCH 08/18] Fix tests.
---
src/useAsync.js | 5 +++--
src/useAsync.spec.js | 22 ++++++++++------------
2 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/src/useAsync.js b/src/useAsync.js
index 675429f2..aa76390a 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -44,7 +44,8 @@ const useAsync = (opts, init) => {
}
const load = () => {
- if (promiseFn && !(initialValue && counter.current === 0)) {
+ const isPreInitialized = initialValue && counter.current === 0
+ if (promiseFn && !isPreInitialized) {
start()
promiseFn(options).then(handleResolve(counter.current), handleReject(counter.current))
}
@@ -67,7 +68,7 @@ const useAsync = (opts, init) => {
isLoading: state.startedAt && (!state.finishedAt || state.finishedAt < state.startedAt),
initialValue,
run,
- reload: () => (lastArgs ? run(...lastArgs) : load()),
+ reload: () => (lastArgs.current ? run(...lastArgs.current) : load()),
cancel: () => {
counter.current++
setState(state => ({ ...state, startedAt: undefined }))
diff --git a/src/useAsync.spec.js b/src/useAsync.spec.js
index 693b3a74..b8695882 100644
--- a/src/useAsync.spec.js
+++ b/src/useAsync.spec.js
@@ -79,15 +79,9 @@ test("useAsync passes finishedAt date when the promise finishes", async () => {
await waitForElement(() => getByText("done"))
})
-test("useAsync passes reload function that re-runs the promise", () => {
- const promiseFn = jest.fn().mockReturnValue(resolveTo())
- const component = (
-
- {({ reload }) => {
- return reload
- }}
-
- )
+test("useAsync passes reload function that re-runs the promise", async () => {
+ const promiseFn = jest.fn().mockReturnValue(resolveTo("done"))
+ const component = {({ reload }) => reload }
const { getByText, rerender } = render(component)
rerender(component)
expect(promiseFn).toHaveBeenCalledTimes(1)
@@ -221,16 +215,20 @@ test("useAsync cancels pending promise when unmounted", async () => {
})
test("useAsync cancels and restarts the promise when promiseFn changes", async () => {
- const promiseFn1 = jest.fn().mockReturnValue(resolveIn(10)("one"))
- const promiseFn2 = jest.fn().mockReturnValue(resolveIn(10)("two"))
+ const promiseFn1 = jest.fn().mockReturnValue(Promise.resolve("one"))
+ const promiseFn2 = jest.fn().mockReturnValue(Promise.resolve("two"))
const onResolve = jest.fn()
const component1 =
const component2 =
const { rerender } = render(component1)
+ await Promise.resolve()
+ rerender(component1)
+ expect(promiseFn1).toHaveBeenCalled()
rerender(component2)
- await resolveIn(10)()
rerender(component2)
+ expect(promiseFn2).toHaveBeenCalled()
expect(onResolve).not.toHaveBeenCalledWith("one")
+ await Promise.resolve()
expect(onResolve).toHaveBeenCalledWith("two")
})
From 735d7b4b1484daf1d2a73720d05dbfad6c93a6af Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Fri, 30 Nov 2018 09:19:55 +0100
Subject: [PATCH 09/18] Use flushEffects instead of the rerender hack.
---
package-lock.json | 42 +++++++++++++++++-------
package.json | 2 +-
src/useAsync.spec.js | 78 ++++++++++++++++++++++----------------------
3 files changed, 70 insertions(+), 52 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b7714b30..0f9219c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -61,6 +61,23 @@
}
}
},
+ "@babel/runtime": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.1.5.tgz",
+ "integrity": "sha512-xKnPpXG/pvK1B90JkwwxSGii90rQGKtzcMt2gI5G6+M0REXaq6rOHsGC2ay6/d0Uje7zzvSzjEzfR3ENhFlrfA==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.12.0"
+ },
+ "dependencies": {
+ "regenerator-runtime": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
+ "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
+ "dev": true
+ }
+ }
+ },
"@sheerun/mutationobserver-shim": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz",
@@ -117,7 +134,7 @@
},
"ansi-escapes": {
"version": "3.1.0",
- "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
"integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
"dev": true
},
@@ -1791,14 +1808,15 @@
"dev": true
},
"dom-testing-library": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.12.0.tgz",
- "integrity": "sha512-eWykEDuKmffXOUKvv4IK00krvC4m+V2/y137M1YccWanUlfUzqS1+3eqm3GMC9qMqCJugfHWn6OpIaQd9rwqcw==",
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.13.0.tgz",
+ "integrity": "sha512-ImIZQrsEPQkmXNFzYmOsCJBjaBcZJe4vRJfP55DhYySD2LL56ACPaJATbXphLGred5efqGC1Q4H3UuqWCZ9Bqg==",
"dev": true,
"requires": {
+ "@babel/runtime": "^7.1.5",
"@sheerun/mutationobserver-shim": "^0.3.2",
"pretty-format": "^23.6.0",
- "wait-for-expect": "^1.0.0"
+ "wait-for-expect": "^1.1.0"
}
},
"domexception": {
@@ -5129,9 +5147,9 @@
}
},
"react-testing-library": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.2.3.tgz",
- "integrity": "sha512-Bw52++7uORuIQnL55lK/WQfppqAc9+8yFG4lWUp/kmSOvYDnt8J9oI5fNCfAGSQi9iIhAv9aNsI2G5rtid0nrA==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.3.1.tgz",
+ "integrity": "sha512-lYhbNhlDRzOkqo+QjDGmG7BpFL8nBNLmCi0npW+VsF8pvhwtu78D4rZ7V3WimRaXFQqOt8VQ0+3CwjGkqV9VUA==",
"dev": true,
"requires": {
"dom-testing-library": "^3.12.0"
@@ -6478,7 +6496,7 @@
},
"strip-eof": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
@@ -6818,9 +6836,9 @@
}
},
"wait-for-expect": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.0.1.tgz",
- "integrity": "sha512-TPZMSxGWUl2DWmqdspLDEy97/S1Mqq0pzbh2A7jTq0WbJurUb5GKli+bai6ayeYdeWTF0rQNWZmUvCVZ9gkrfA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.0.tgz",
+ "integrity": "sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==",
"dev": true
},
"walker": {
diff --git a/package.json b/package.json
index 50700faa..55d36c56 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"jest-dom": "2.1.0",
"react": "16.7.0-alpha.0",
"react-dom": "16.7.0-alpha.0",
- "react-testing-library": "5.2.3",
+ "react-testing-library": "5.3.1",
"rimraf": "2.6.2"
},
"jest": {
diff --git a/src/useAsync.spec.js b/src/useAsync.spec.js
index b8695882..2ed21ed9 100644
--- a/src/useAsync.spec.js
+++ b/src/useAsync.spec.js
@@ -1,6 +1,6 @@
import "jest-dom/extend-expect"
import React from "react"
-import { render, fireEvent, cleanup, waitForElement } from "react-testing-library"
+import { render, fireEvent, cleanup, waitForElement, flushEffects } from "react-testing-library"
import { useAsync } from "."
afterEach(cleanup)
@@ -13,16 +13,16 @@ const Async = ({ children = () => null, ...props }) => children(useAsync(props))
test("useAsync returns render props", async () => {
const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
const component = {({ data }) => data || null}
- const { getByText, rerender } = render(component)
- rerender(component) // needed to trigger useEffect
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("done"))
})
test("useAsync passes rejection error to children as render prop", async () => {
const promiseFn = () => Promise.reject("oops")
const component = {({ error }) => error || null}
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("oops"))
})
@@ -37,8 +37,8 @@ test("useAsync passes isLoading boolean while the promise is running", async ()
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("done"))
expect(states).toEqual([true, true, false])
})
@@ -56,8 +56,8 @@ test("useAsync passes startedAt date when the promise starts", async () => {
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("started"))
})
@@ -74,16 +74,16 @@ test("useAsync passes finishedAt date when the promise finishes", async () => {
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("done"))
})
test("useAsync passes reload function that re-runs the promise", async () => {
const promiseFn = jest.fn().mockReturnValue(resolveTo("done"))
const component = {({ reload }) => reload }
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
expect(promiseFn).toHaveBeenCalledTimes(1)
fireEvent.click(getByText("reload"))
expect(promiseFn).toHaveBeenCalledTimes(2)
@@ -104,14 +104,14 @@ test("useAsync re-runs the promise when the value of 'watch' changes", () => {
}
const promiseFn = jest.fn().mockReturnValue(resolveTo())
const component = {count => }
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
expect(promiseFn).toHaveBeenCalledTimes(1)
fireEvent.click(getByText("increment"))
- rerender(component)
+ flushEffects()
expect(promiseFn).toHaveBeenCalledTimes(2)
fireEvent.click(getByText("increment"))
- rerender(component)
+ flushEffects()
expect(promiseFn).toHaveBeenCalledTimes(3)
})
@@ -125,8 +125,8 @@ test("useAsync runs deferFn only when explicitly invoked, passing arguments and
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
expect(deferFn).not.toHaveBeenCalled()
fireEvent.click(getByText("run"))
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn, foo: "bar" }))
@@ -149,8 +149,8 @@ test("useAsync reload uses the arguments of the previous run", () => {
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
expect(deferFn).not.toHaveBeenCalled()
fireEvent.click(getByText("run"))
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }))
@@ -187,8 +187,8 @@ test("useAsync invokes onResolve callback when promise resolves", async () => {
const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
const onResolve = jest.fn()
const component =
- const { rerender } = render(component)
- rerender(component)
+ render(component)
+ flushEffects()
await Promise.resolve()
expect(onResolve).toHaveBeenCalledWith("ok")
})
@@ -197,8 +197,8 @@ test("useAsync invokes onReject callback when promise rejects", async () => {
const promiseFn = jest.fn().mockReturnValue(Promise.reject("err"))
const onReject = jest.fn()
const component =
- const { rerender } = render(component)
- rerender(component)
+ render(component)
+ flushEffects()
await Promise.resolve()
expect(onReject).toHaveBeenCalledWith("err")
})
@@ -207,8 +207,8 @@ test("useAsync cancels pending promise when unmounted", async () => {
const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
const onResolve = jest.fn()
const component =
- const { unmount, rerender } = render(component)
- rerender(component)
+ const { unmount } = render(component)
+ flushEffects()
unmount()
await Promise.resolve()
expect(onResolve).not.toHaveBeenCalled()
@@ -222,10 +222,10 @@ test("useAsync cancels and restarts the promise when promiseFn changes", async (
const component2 =
const { rerender } = render(component1)
await Promise.resolve()
- rerender(component1)
+ flushEffects()
expect(promiseFn1).toHaveBeenCalled()
rerender(component2)
- rerender(component2)
+ flushEffects()
expect(promiseFn2).toHaveBeenCalled()
expect(onResolve).not.toHaveBeenCalledWith("one")
await Promise.resolve()
@@ -235,8 +235,8 @@ test("useAsync cancels and restarts the promise when promiseFn changes", async (
test("useAsync does not run promiseFn on mount when initialValue is provided", () => {
const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
const component =
- const { rerender } = render(component)
- rerender(component)
+ render(component)
+ flushEffects()
expect(promiseFn).not.toHaveBeenCalled()
})
@@ -251,8 +251,8 @@ test("useAsync does not start loading when using initialValue", async () => {
}}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("done"))
expect(states).toEqual([false])
})
@@ -264,8 +264,8 @@ test("useAsync passes initialValue to children immediately", async () => {
{({ data }) => data}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("done"))
})
@@ -277,8 +277,8 @@ test("useAsync sets error instead of data when initialValue is an Error object",
{({ error }) => error.message}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("oops"))
})
@@ -296,8 +296,8 @@ test("useAsync can be nested", async () => {
)}
)
- const { getByText, rerender } = render(component)
- rerender(component)
+ const { getByText } = render(component)
+ flushEffects()
await waitForElement(() => getByText("outer undefined"))
await waitForElement(() => getByText("outer inner"))
})
From e213f0e6fd79e7df4b3f4e58321a9132ad1bc795 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Fri, 30 Nov 2018 10:02:39 +0100
Subject: [PATCH 10/18] Fix static member definitions.
---
typings/index.d.ts | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 326145e7..18a0f32d 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -26,11 +26,13 @@ interface AsyncState {
setError: (error: Error, callback?: () => void) => Error
}
-declare class Async extends React.Component, AsyncState> {
- static Pending: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
- static Loading: React.FunctionComponent<{ children?: AsyncChildren; initial?: boolean }>
- static Resolved: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
- static Rejected: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
+declare class Async extends React.Component, AsyncState> {}
+
+namespace Async {
+ export const Pending: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
+ export const Loading: React.FunctionComponent<{ children?: AsyncChildren; initial?: boolean }>
+ export const Resolved: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
+ export const Rejected: React.FunctionComponent<{ children?: AsyncChildren; persist?: boolean }>
}
declare function createInstance(defaultProps?: AsyncProps): Async
From 907d226a78f598d033634b5d44bc19e026f6a9e7 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Fri, 30 Nov 2018 11:23:41 +0100
Subject: [PATCH 11/18] Add typings for useAsync.
---
src/useAsync.js | 4 +++-
typings/index.d.ts | 10 ++++++++--
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/useAsync.js b/src/useAsync.js
index aa76390a..c77b4fc7 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -81,7 +81,9 @@ const useAsync = (opts, init) => {
}
const unsupported = () => {
- throw new Error("useAsync requires react@16.7.0 or later")
+ throw new Error(
+ "useAsync requires react@16.7.0 or later. Upgrade your React version or use the component instead."
+ )
}
export default (useState ? useAsync : unsupported)
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 9bbf58b8..0196ec9e 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -1,14 +1,18 @@
import * as React from "react"
type AsyncChildren = ((state: AsyncState) => React.ReactNode) | React.ReactNode
+type PromiseFn = (props: object) => Promise
-interface AsyncProps {
- promiseFn?: (props: object) => Promise
+interface AsyncOptions {
+ promiseFn?: PromiseFn
deferFn?: (...args) => Promise
watch?: any
initialValue?: T
onResolve?: (data: T) => void
onError?: (error: Error) => void
+}
+
+interface AsyncProps extends AsyncOptions {
children?: AsyncChildren
}
@@ -37,4 +41,6 @@ declare namespace Async {
declare function createInstance(defaultProps?: AsyncProps): Async
+export function useAsync(opts: AsyncOptions | PromiseFn, init?: T): AsyncState
+
export default createInstance
From 8cf78ed222e6626bfd5b490678b980b1f27b20b9 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Fri, 30 Nov 2018 11:26:48 +0100
Subject: [PATCH 12/18] Sync package-lock.
---
package-lock.json | 214 ++++++++++++++++++++++++++++++----------------
1 file changed, 140 insertions(+), 74 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 82e96a4b..331ec44a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2175,24 +2175,28 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"aproba": {
"version": "1.2.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz",
+ "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
"dev": true,
"optional": true,
"requires": {
@@ -2202,12 +2206,14 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -2216,34 +2222,40 @@
},
"chownr": {
"version": "1.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
+ "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true,
"optional": true
},
"debug": {
"version": "2.6.9",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"optional": true,
"requires": {
@@ -2252,25 +2264,29 @@
},
"deep-extend": {
"version": "0.5.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
+ "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==",
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
+ "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2279,13 +2295,15 @@
},
"fs.realpath": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"optional": true,
"requires": {
@@ -2301,7 +2319,8 @@
},
"glob": {
"version": "7.1.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2315,13 +2334,15 @@
},
"has-unicode": {
"version": "2.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.21",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz",
+ "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==",
"dev": true,
"optional": true,
"requires": {
@@ -2330,7 +2351,8 @@
},
"ignore-walk": {
"version": "3.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
+ "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2339,7 +2361,8 @@
},
"inflight": {
"version": "1.0.6",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"optional": true,
"requires": {
@@ -2349,18 +2372,21 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"ini": {
"version": "1.3.5",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
@@ -2368,13 +2394,15 @@
},
"isarray": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -2382,12 +2410,14 @@
},
"minimist": {
"version": "0.0.8",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"minipass": {
"version": "2.2.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
+ "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.1",
@@ -2396,7 +2426,8 @@
},
"minizlib": {
"version": "1.1.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz",
+ "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==",
"dev": true,
"optional": true,
"requires": {
@@ -2405,7 +2436,8 @@
},
"mkdirp": {
"version": "0.5.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
@@ -2413,13 +2445,15 @@
},
"ms": {
"version": "2.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true,
"optional": true
},
"needle": {
"version": "2.2.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz",
+ "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==",
"dev": true,
"optional": true,
"requires": {
@@ -2430,7 +2464,8 @@
},
"node-pre-gyp": {
"version": "0.10.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz",
+ "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2448,7 +2483,8 @@
},
"nopt": {
"version": "4.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+ "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true,
"optional": true,
"requires": {
@@ -2458,13 +2494,15 @@
},
"npm-bundled": {
"version": "1.0.3",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz",
+ "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==",
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.1.10",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz",
+ "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==",
"dev": true,
"optional": true,
"requires": {
@@ -2474,7 +2512,8 @@
},
"npmlog": {
"version": "4.1.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"optional": true,
"requires": {
@@ -2486,18 +2525,21 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
"object-assign": {
"version": "4.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
@@ -2505,19 +2547,22 @@
},
"os-homedir": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"optional": true,
"requires": {
@@ -2527,19 +2572,22 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.7",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz",
+ "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==",
"dev": true,
"optional": true,
"requires": {
@@ -2551,7 +2599,8 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
}
@@ -2559,7 +2608,8 @@
},
"readable-stream": {
"version": "2.3.6",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"optional": true,
"requires": {
@@ -2574,7 +2624,8 @@
},
"rimraf": {
"version": "2.6.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"optional": true,
"requires": {
@@ -2583,42 +2634,49 @@
},
"safe-buffer": {
"version": "5.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true,
"optional": true
},
"semver": {
"version": "5.5.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
@@ -2628,7 +2686,8 @@
},
"string_decoder": {
"version": "1.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"optional": true,
"requires": {
@@ -2637,7 +2696,8 @@
},
"strip-ansi": {
"version": "3.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
@@ -2645,13 +2705,15 @@
},
"strip-json-comments": {
"version": "2.0.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz",
+ "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==",
"dev": true,
"optional": true,
"requires": {
@@ -2666,13 +2728,15 @@
},
"util-deprecate": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
+ "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
"dev": true,
"optional": true,
"requires": {
@@ -2681,12 +2745,14 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"yallist": {
"version": "3.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
+ "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"dev": true
}
}
@@ -5123,27 +5189,27 @@
}
},
"react": {
- "version": "16.6.3",
- "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
- "integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
+ "version": "16.7.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.7.0-alpha.0.tgz",
+ "integrity": "sha512-V0za4H01aoAF0SdzahHepvfvzTQ1xxkgMX4z8uKzn+wzZAlVk0IVpleqyxZWluqmdftNedj6fIIZRO/rVYVFvQ==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.11.2"
+ "scheduler": "^0.11.0-alpha.0"
}
},
"react-dom": {
- "version": "16.6.3",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz",
- "integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==",
+ "version": "16.7.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0-alpha.0.tgz",
+ "integrity": "sha512-/XUn1ldxmoV2B7ov0rWT5LMZaaHMlF9GGLkUsmPRxmWTJwRDOuAPXidSaSlmR/VOhDSI1s+v3+KzFqhhDFJxYA==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.11.2"
+ "scheduler": "^0.11.0-alpha.0"
}
},
"react-testing-library": {
From e3dadc9fc54eaa342cd85a4844ce1726f6d8b674 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Fri, 30 Nov 2018 15:14:14 +0100
Subject: [PATCH 13/18] Make sure the right React version is installed to test
useAsync with.
---
.travis.yml | 2 +-
package.json | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 2286c93e..59a494e1 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 test:compat && npm run test:hook
after_success:
- bash <(curl -s https://codecov.io/bash) -e TRAVIS_NODE_VERSION
diff --git a/package.json b/package.json
index 8886ad04..8d0264fa 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,13 @@
],
"scripts": {
"build": "rimraf lib && babel src -d lib --ignore '**/*spec.js'",
- "test": "jest src",
+ "test": "jest src/spec.js",
"test:watch": "npm run test -- --watch",
"test:compat": "npm run test:backwards && npm run test:forwards && npm run test:latest",
"test:backwards": "npm i react@16.3.1 react-dom@16.3.1 && npm test",
"test:forwards": "npm i react@next react-dom@next && npm test",
"test:latest": "npm i react@latest react-dom@latest && npm test",
+ "test:hook": "npm i react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 && jest src/useAsync.spec.js",
"prepublishOnly": "npm run test:compat && npm run build"
},
"dependencies": {},
From 6fdf304a52e31cfaa9f3b3ae41dfe378adc7ad06 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Sun, 30 Dec 2018 22:17:33 +0100
Subject: [PATCH 14/18] Add test for cancel().
---
src/useAsync.spec.js | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/useAsync.spec.js b/src/useAsync.spec.js
index 2ed21ed9..a82d411d 100644
--- a/src/useAsync.spec.js
+++ b/src/useAsync.spec.js
@@ -81,7 +81,9 @@ test("useAsync passes finishedAt date when the promise finishes", async () => {
test("useAsync passes reload function that re-runs the promise", async () => {
const promiseFn = jest.fn().mockReturnValue(resolveTo("done"))
- const component = {({ reload }) => reload }
+ const component = (
+ {({ reload }) => reload }
+ )
const { getByText } = render(component)
flushEffects()
expect(promiseFn).toHaveBeenCalledTimes(1)
@@ -134,6 +136,21 @@ test("useAsync runs deferFn only when explicitly invoked, passing arguments and
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn, foo: "bar" }))
})
+test("useAsync cancel will prevent the resolved promise from propagating", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component = (
+
+ {({ cancel }) => cancel }
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ fireEvent.click(getByText("cancel"))
+ await Promise.resolve()
+ expect(onResolve).not.toHaveBeenCalled()
+})
+
test("useAsync reload uses the arguments of the previous run", () => {
let counter = 1
const deferFn = jest.fn().mockReturnValue(resolveTo())
From 46f8b8c209c32f8e2de7c9c99e4a2cfc7bf52290 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Sun, 30 Dec 2018 22:18:32 +0100
Subject: [PATCH 15/18] Fix coverage reporting.
---
package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 8d0264fa..0817e552 100644
--- a/package.json
+++ b/package.json
@@ -27,13 +27,13 @@
],
"scripts": {
"build": "rimraf lib && babel src -d lib --ignore '**/*spec.js'",
- "test": "jest src/spec.js",
+ "test": "jest src/spec.js --collectCoverageFrom=src/index.js",
"test:watch": "npm run test -- --watch",
"test:compat": "npm run test:backwards && npm run test:forwards && npm run test:latest",
"test:backwards": "npm i react@16.3.1 react-dom@16.3.1 && npm test",
"test:forwards": "npm i react@next react-dom@next && npm test",
"test:latest": "npm i react@latest react-dom@latest && npm test",
- "test:hook": "npm i react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 && jest src/useAsync.spec.js",
+ "test:hook": "npm i react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 && jest src/useAsync.spec.js --collectCoverageFrom=src/useAsync.js",
"prepublishOnly": "npm run test:compat && npm run build"
},
"dependencies": {},
From a1ffcb7bbe6be4191ae38232021fbbc36e2f658f Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Sun, 30 Dec 2018 22:27:24 +0100
Subject: [PATCH 16/18] Fix conflict.
---
package.json | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/package.json b/package.json
index 51e750dc..9744c82d 100644
--- a/package.json
+++ b/package.json
@@ -56,17 +56,11 @@
"eslint-plugin-react": "7.12.0",
"jest": "23.6.0",
"jest-dom": "2.1.0",
-<<<<<<< HEAD
- "react": "16.7.0-alpha.0",
- "react-dom": "16.7.0-alpha.0",
- "react-testing-library": "5.3.1",
- "rimraf": "2.6.2"
-=======
"prettier": "1.15.3",
"react": "16.6.3",
"react-dom": "16.6.3",
- "react-testing-library": "5.2.3"
->>>>>>> master
+ "react-testing-library": "5.4.2",
+ "rimraf": "2.6.2"
},
"jest": {
"coverageDirectory": "./coverage/",
From 6b1f5218f52592a950c8682b6a281f153ed10569 Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Sun, 30 Dec 2018 22:32:38 +0100
Subject: [PATCH 17/18] Restructure tests.
---
src/useAsync.spec.js | 576 ++++++++++++++++++++++---------------------
1 file changed, 289 insertions(+), 287 deletions(-)
diff --git a/src/useAsync.spec.js b/src/useAsync.spec.js
index a82d411d..02e84b49 100644
--- a/src/useAsync.spec.js
+++ b/src/useAsync.spec.js
@@ -10,311 +10,313 @@ const resolveTo = resolveIn(0)
const Async = ({ children = () => null, ...props }) => children(useAsync(props))
-test("useAsync returns render props", async () => {
- const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
- const component = {({ data }) => data || null}
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("done"))
-})
+describe("useAsync", () => {
+ test("returns render props", async () => {
+ const promiseFn = () => new Promise(resolve => setTimeout(resolve, 0, "done"))
+ const component = {({ data }) => data || null}
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("done"))
+ })
-test("useAsync passes rejection error to children as render prop", async () => {
- const promiseFn = () => Promise.reject("oops")
- const component = {({ error }) => error || null}
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("oops"))
-})
+ test("passes rejection error to children as render prop", async () => {
+ const promiseFn = () => Promise.reject("oops")
+ const component = {({ error }) => error || null}
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("oops"))
+ })
-test("useAsync passes isLoading boolean while the promise is running", async () => {
- const promiseFn = () => resolveTo("done")
- const states = []
- const component = (
-
- {({ data, isLoading }) => {
- states.push(isLoading)
- return data || null
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("done"))
- expect(states).toEqual([true, true, false])
-})
-
-test("useAsync passes startedAt date when the promise starts", async () => {
- const promiseFn = () => resolveTo("done")
- const component = (
-
- {({ startedAt }) => {
- if (startedAt) {
- expect(startedAt.getTime()).toBeCloseTo(new Date().getTime(), -2)
- return "started"
- }
- return null
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("started"))
-})
-
-test("useAsync passes finishedAt date when the promise finishes", async () => {
- const promiseFn = () => resolveTo("done")
- const component = (
-
- {({ data, finishedAt }) => {
- if (finishedAt) {
- expect(finishedAt.getTime()).toBeCloseTo(new Date().getTime(), -1)
+ test("passes isLoading boolean while the promise is running", async () => {
+ const promiseFn = () => resolveTo("done")
+ const states = []
+ const component = (
+
+ {({ data, isLoading }) => {
+ states.push(isLoading)
return data || null
- }
- return null
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("done"))
-})
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("done"))
+ expect(states).toEqual([true, true, false])
+ })
-test("useAsync passes reload function that re-runs the promise", async () => {
- const promiseFn = jest.fn().mockReturnValue(resolveTo("done"))
- const component = (
- {({ reload }) => reload }
- )
- const { getByText } = render(component)
- flushEffects()
- expect(promiseFn).toHaveBeenCalledTimes(1)
- fireEvent.click(getByText("reload"))
- expect(promiseFn).toHaveBeenCalledTimes(2)
-})
-
-test("useAsync re-runs the promise when the value of 'watch' changes", () => {
- class Counter extends React.Component {
- state = { count: 0 }
- inc = () => this.setState(state => ({ count: state.count + 1 }))
- render() {
- return (
-
- increment
- {this.props.children(this.state.count)}
-
- )
- }
- }
- const promiseFn = jest.fn().mockReturnValue(resolveTo())
- const component = {count => }
- const { getByText } = render(component)
- flushEffects()
- expect(promiseFn).toHaveBeenCalledTimes(1)
- fireEvent.click(getByText("increment"))
- flushEffects()
- expect(promiseFn).toHaveBeenCalledTimes(2)
- fireEvent.click(getByText("increment"))
- flushEffects()
- expect(promiseFn).toHaveBeenCalledTimes(3)
-})
+ test("passes startedAt date when the promise starts", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ startedAt }) => {
+ if (startedAt) {
+ expect(startedAt.getTime()).toBeCloseTo(new Date().getTime(), -2)
+ return "started"
+ }
+ return null
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("started"))
+ })
-test("useAsync runs deferFn only when explicitly invoked, passing arguments and props", () => {
- let counter = 1
- const deferFn = jest.fn().mockReturnValue(resolveTo())
- const component = (
-
- {({ run }) => {
- return run("go", counter++)}>run
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- expect(deferFn).not.toHaveBeenCalled()
- fireEvent.click(getByText("run"))
- expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn, foo: "bar" }))
- fireEvent.click(getByText("run"))
- expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn, foo: "bar" }))
-})
+ test("passes finishedAt date when the promise finishes", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ data, finishedAt }) => {
+ if (finishedAt) {
+ expect(finishedAt.getTime()).toBeCloseTo(new Date().getTime(), -1)
+ return data || null
+ }
+ return null
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("done"))
+ })
-test("useAsync cancel will prevent the resolved promise from propagating", async () => {
- const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
- const onResolve = jest.fn()
- const component = (
-
- {({ cancel }) => cancel }
-
- )
- const { getByText } = render(component)
- flushEffects()
- fireEvent.click(getByText("cancel"))
- await Promise.resolve()
- expect(onResolve).not.toHaveBeenCalled()
-})
+ test("passes reload function that re-runs the promise", async () => {
+ const promiseFn = jest.fn().mockReturnValue(resolveTo("done"))
+ const component = (
+ {({ reload }) => reload }
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ expect(promiseFn).toHaveBeenCalledTimes(1)
+ fireEvent.click(getByText("reload"))
+ expect(promiseFn).toHaveBeenCalledTimes(2)
+ })
-test("useAsync reload uses the arguments of the previous run", () => {
- let counter = 1
- const deferFn = jest.fn().mockReturnValue(resolveTo())
- const component = (
-
- {({ run, reload }) => {
+ test("re-runs the promise when the value of 'watch' changes", () => {
+ class Counter extends React.Component {
+ state = { count: 0 }
+ inc = () => this.setState(state => ({ count: state.count + 1 }))
+ render() {
return (
- run("go", counter++)}>run
- reload
+ increment
+ {this.props.children(this.state.count)}
)
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- expect(deferFn).not.toHaveBeenCalled()
- fireEvent.click(getByText("run"))
- expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }))
- fireEvent.click(getByText("run"))
- expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
- fireEvent.click(getByText("reload"))
- expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
-})
+ }
+ }
+ const promiseFn = jest.fn().mockReturnValue(resolveTo())
+ const component = {count => }
+ const { getByText } = render(component)
+ flushEffects()
+ expect(promiseFn).toHaveBeenCalledTimes(1)
+ fireEvent.click(getByText("increment"))
+ flushEffects()
+ expect(promiseFn).toHaveBeenCalledTimes(2)
+ fireEvent.click(getByText("increment"))
+ flushEffects()
+ expect(promiseFn).toHaveBeenCalledTimes(3)
+ })
+
+ test("runs deferFn only when explicitly invoked, passing arguments and props", () => {
+ let counter = 1
+ const deferFn = jest.fn().mockReturnValue(resolveTo())
+ const component = (
+
+ {({ run }) => {
+ return run("go", counter++)}>run
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ expect(deferFn).not.toHaveBeenCalled()
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn, foo: "bar" }))
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn, foo: "bar" }))
+ })
+
+ test("cancel will prevent the resolved promise from propagating", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component = (
+
+ {({ cancel }) => cancel }
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ fireEvent.click(getByText("cancel"))
+ await Promise.resolve()
+ expect(onResolve).not.toHaveBeenCalled()
+ })
+
+ test("reload uses the arguments of the previous run", () => {
+ let counter = 1
+ const deferFn = jest.fn().mockReturnValue(resolveTo())
+ const component = (
+
+ {({ run, reload }) => {
+ return (
+
+ run("go", counter++)}>run
+ reload
+
+ )
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ expect(deferFn).not.toHaveBeenCalled()
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }))
+ fireEvent.click(getByText("run"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
+ fireEvent.click(getByText("reload"))
+ expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }))
+ })
-test("useAsync only accepts the last invocation of the promise", async () => {
- let i = 0
- const resolves = [resolveIn(10)("a"), resolveIn(20)("b"), resolveIn(10)("c")]
- const component = (
- resolves[i]}>
- {({ data, run }) => {
- if (data) {
- expect(data).toBe("c")
- return "done"
- }
- return run(i)}>run
- }}
-
- )
- const { getByText } = render(component)
- fireEvent.click(getByText("run"))
- i++
- fireEvent.click(getByText("run"))
- i++
- fireEvent.click(getByText("run"))
- await waitForElement(() => getByText("done"))
-})
+ test("only accepts the last invocation of the promise", async () => {
+ let i = 0
+ const resolves = [resolveIn(10)("a"), resolveIn(20)("b"), resolveIn(10)("c")]
+ const component = (
+ resolves[i]}>
+ {({ data, run }) => {
+ if (data) {
+ expect(data).toBe("c")
+ return "done"
+ }
+ return run(i)}>run
+ }}
+
+ )
+ const { getByText } = render(component)
+ fireEvent.click(getByText("run"))
+ i++
+ fireEvent.click(getByText("run"))
+ i++
+ fireEvent.click(getByText("run"))
+ await waitForElement(() => getByText("done"))
+ })
-test("useAsync invokes onResolve callback when promise resolves", async () => {
- const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
- const onResolve = jest.fn()
- const component =
- render(component)
- flushEffects()
- await Promise.resolve()
- expect(onResolve).toHaveBeenCalledWith("ok")
-})
+ test("invokes onResolve callback when promise resolves", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component =
+ render(component)
+ flushEffects()
+ await Promise.resolve()
+ expect(onResolve).toHaveBeenCalledWith("ok")
+ })
-test("useAsync invokes onReject callback when promise rejects", async () => {
- const promiseFn = jest.fn().mockReturnValue(Promise.reject("err"))
- const onReject = jest.fn()
- const component =
- render(component)
- flushEffects()
- await Promise.resolve()
- expect(onReject).toHaveBeenCalledWith("err")
-})
+ test("invokes onReject callback when promise rejects", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.reject("err"))
+ const onReject = jest.fn()
+ const component =
+ render(component)
+ flushEffects()
+ await Promise.resolve()
+ expect(onReject).toHaveBeenCalledWith("err")
+ })
-test("useAsync cancels pending promise when unmounted", async () => {
- const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
- const onResolve = jest.fn()
- const component =
- const { unmount } = render(component)
- flushEffects()
- unmount()
- await Promise.resolve()
- expect(onResolve).not.toHaveBeenCalled()
-})
+ test("cancels pending promise when unmounted", async () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
+ const onResolve = jest.fn()
+ const component =
+ const { unmount } = render(component)
+ flushEffects()
+ unmount()
+ await Promise.resolve()
+ expect(onResolve).not.toHaveBeenCalled()
+ })
-test("useAsync cancels and restarts the promise when promiseFn changes", async () => {
- const promiseFn1 = jest.fn().mockReturnValue(Promise.resolve("one"))
- const promiseFn2 = jest.fn().mockReturnValue(Promise.resolve("two"))
- const onResolve = jest.fn()
- const component1 =
- const component2 =
- const { rerender } = render(component1)
- await Promise.resolve()
- flushEffects()
- expect(promiseFn1).toHaveBeenCalled()
- rerender(component2)
- flushEffects()
- expect(promiseFn2).toHaveBeenCalled()
- expect(onResolve).not.toHaveBeenCalledWith("one")
- await Promise.resolve()
- expect(onResolve).toHaveBeenCalledWith("two")
-})
+ test("cancels and restarts the promise when promiseFn changes", async () => {
+ const promiseFn1 = jest.fn().mockReturnValue(Promise.resolve("one"))
+ const promiseFn2 = jest.fn().mockReturnValue(Promise.resolve("two"))
+ const onResolve = jest.fn()
+ const component1 =
+ const component2 =
+ const { rerender } = render(component1)
+ await Promise.resolve()
+ flushEffects()
+ expect(promiseFn1).toHaveBeenCalled()
+ rerender(component2)
+ flushEffects()
+ expect(promiseFn2).toHaveBeenCalled()
+ expect(onResolve).not.toHaveBeenCalledWith("one")
+ await Promise.resolve()
+ expect(onResolve).toHaveBeenCalledWith("two")
+ })
-test("useAsync does not run promiseFn on mount when initialValue is provided", () => {
- const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
- const component =
- render(component)
- flushEffects()
- expect(promiseFn).not.toHaveBeenCalled()
-})
+ test("does not run promiseFn on mount when initialValue is provided", () => {
+ const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
+ const component =
+ render(component)
+ flushEffects()
+ expect(promiseFn).not.toHaveBeenCalled()
+ })
-test("useAsync does not start loading when using initialValue", async () => {
- const promiseFn = () => resolveTo("done")
- const states = []
- const component = (
-
- {({ data, isLoading }) => {
- states.push(isLoading)
- return data
- }}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("done"))
- expect(states).toEqual([false])
-})
+ test("does not start loading when using initialValue", async () => {
+ const promiseFn = () => resolveTo("done")
+ const states = []
+ const component = (
+
+ {({ data, isLoading }) => {
+ states.push(isLoading)
+ return data
+ }}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("done"))
+ expect(states).toEqual([false])
+ })
-test("useAsync passes initialValue to children immediately", async () => {
- const promiseFn = () => resolveTo("done")
- const component = (
-
- {({ data }) => data}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("done"))
-})
+ test("passes initialValue to children immediately", async () => {
+ const promiseFn = () => resolveTo("done")
+ const component = (
+
+ {({ data }) => data}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("done"))
+ })
-test("useAsync sets error instead of data when initialValue is an Error object", async () => {
- const promiseFn = () => resolveTo("done")
- const error = new Error("oops")
- const component = (
-
- {({ error }) => error.message}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("oops"))
-})
+ test("sets error instead of data when initialValue is an Error object", async () => {
+ const promiseFn = () => resolveTo("done")
+ const error = new Error("oops")
+ const component = (
+
+ {({ error }) => error.message}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("oops"))
+ })
-test("useAsync can be nested", async () => {
- const outerFn = () => resolveIn(0)("outer")
- const innerFn = () => resolveIn(100)("inner")
- const component = (
-
- {({ data: outer }) => (
-
- {({ data: inner }) => {
- return outer + " " + inner
- }}
-
- )}
-
- )
- const { getByText } = render(component)
- flushEffects()
- await waitForElement(() => getByText("outer undefined"))
- await waitForElement(() => getByText("outer inner"))
-})
+ test("can be nested", async () => {
+ const outerFn = () => resolveIn(0)("outer")
+ const innerFn = () => resolveIn(100)("inner")
+ const component = (
+
+ {({ data: outer }) => (
+
+ {({ data: inner }) => {
+ return outer + " " + inner
+ }}
+
+ )}
+
+ )
+ const { getByText } = render(component)
+ flushEffects()
+ await waitForElement(() => getByText("outer undefined"))
+ await waitForElement(() => getByText("outer inner"))
+ })
+})
\ No newline at end of file
From 73e6cb2e11a1bc981fa9be7bc120c33ff887e14f Mon Sep 17 00:00:00 2001
From: Gert Hengeveld
Date: Sun, 30 Dec 2018 22:39:34 +0100
Subject: [PATCH 18/18] Use latest React version with hooks.
---
package.json | 2 +-
src/useAsync.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 9744c82d..20b8e26e 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"test:backwards": "npm i react@16.3.1 react-dom@16.3.1 && npm test",
"test:forwards": "npm i react@next react-dom@next && npm test",
"test:latest": "npm i react@latest react-dom@latest && npm test",
- "test:hook": "npm i react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 && jest src/useAsync.spec.js --collectCoverageFrom=src/useAsync.js",
+ "test:hook": "npm i react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2 && jest src/useAsync.spec.js --collectCoverageFrom=src/useAsync.js",
"prepublishOnly": "npm run lint && npm run test:compat && npm run test:hook && npm run build"
},
"dependencies": {},
diff --git a/src/useAsync.js b/src/useAsync.js
index c77b4fc7..4f6319c6 100644
--- a/src/useAsync.js
+++ b/src/useAsync.js
@@ -82,7 +82,7 @@ const useAsync = (opts, init) => {
const unsupported = () => {
throw new Error(
- "useAsync requires react@16.7.0 or later. Upgrade your React version or use the component instead."
+ "useAsync requires react@16.7.0-alpha. Upgrade your React version or use the component instead."
)
}