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 + }} + + ) + 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 ( +
+ + {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 + }} + + ) + 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 ( +
+ + +
+ ) + }} +
+ ) + 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 + }} + + ) + 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 - }} - - ) +test("useAsync passes reload function that re-runs the promise", async () => { + const promiseFn = jest.fn().mockReturnValue(resolveTo("done")) + const component = {({ 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 }) => } - 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 }) => } + const component = ( + {({ 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 }) => } + + ) + 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 }) => } - ) - 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 ( -
- - {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 - }} - - ) - 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 }) => } - - ) - 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 }) => } + ) + 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 (
- - + + {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 + }} + + ) + 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 }) => } + + ) + 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 ( +
+ + +
+ ) + }} +
+ ) + 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 - }} - - ) - 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 + }} + + ) + 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." ) }