diff --git a/README.md b/README.md index e8ba202ec9..d465d25c72 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@
- [**Side-effects**](./docs/Side-effects.md) - [`useAsync`](./docs/useAsync.md) — resolves an `async` function. + - [`useAsyncCallback`](./docs/useAsyncCallback.md) — state management for async callback - [`useAsyncRetry`](./docs/useAsyncRetry.md) — `useAsync` with `retry()` method. - [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard. - [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo) diff --git a/docs/useAsyncCallback.md b/docs/useAsyncCallback.md new file mode 100644 index 0000000000..4e54eab96a --- /dev/null +++ b/docs/useAsyncCallback.md @@ -0,0 +1,36 @@ +# `useAsyncCallback` + +React hook that returns state and a callback for an `async` function or a +function that returns a promise. The state is of the same shape as `useAsync`. + +## Usage + +```jsx +import {useAsyncCallback} from 'react-use'; + +const Demo = (url) => { + const [state, fetch] = useAsyncCallback(async () => { + const response = await fetch(url); + const result = await response.text(); + return result + }), [url]; + + return ( +
+ {state.loading + ?
Loading...
+ : state.error + ?
Error: {state.error.message}
+ : state.value &&
Value: {state.value}
+ } + +
+ ); +}; +``` + +## Reference + +```ts +useAsyncCallback(fn, deps?: any[]); +``` diff --git a/src/__stories__/useAsyncCallback.story.tsx b/src/__stories__/useAsyncCallback.story.tsx new file mode 100644 index 0000000000..ddb2941343 --- /dev/null +++ b/src/__stories__/useAsyncCallback.story.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {useAsyncCallback} from '..'; +import ShowDocs from './util/ShowDocs'; + +const fn = () => + new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.5) { + reject(new Error('Random error!')); + } else { + resolve('RESOLVED'); + } + }, 1000); + }); + +const Demo = () => { + const [{loading, error, value}, callback] = useAsyncCallback(fn); + + return ( +
+ {loading + ?
Loading...
+ : error + ?
Error: {error.message}
+ : value &&
Value: {value}
+ } + +
+ ); +}; + +storiesOf('Side effects|useAsyncCallback', module) + .add('Docs', () => ) + .add('Demo', () => ) diff --git a/src/index.ts b/src/index.ts index 6176465943..c0ff7e4d12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import createMemo from './createMemo'; import useAsync from './useAsync'; +import useAsyncCallback from './useAsyncCallback'; import useAsyncRetry from './useAsyncRetry'; import useAudio from './useAudio'; import useBattery from './useBattery'; @@ -71,6 +72,7 @@ import useUpdateEffect from './useUpdateEffect' export { createMemo, useAsync, + useAsyncCallback, useAsyncRetry, useAudio, useBattery, diff --git a/src/useAsyncCallback.ts b/src/useAsyncCallback.ts new file mode 100644 index 0000000000..d89f166679 --- /dev/null +++ b/src/useAsyncCallback.ts @@ -0,0 +1,48 @@ +import { useState, useEffect, useCallback, DependencyList } from 'react'; +import useRefMounted from "./useRefMounted" + +export type AsyncState = +| { + loading: boolean; + error?: undefined; + value?: undefined; +} +| { + loading: false; + error: Error; + value?: undefined; +} +| { + loading: false; + error?: undefined; + value: T; +}; + +const useAsyncCallback = (fn: () => Promise, deps: DependencyList = []): [AsyncState, () => void] => { + const [state, set] = useState>({ + loading: false + }); + + const mounted = useRefMounted(); + + const callback = useCallback(() => { + set({loading: true}); + + fn().then( + value => { + if (mounted.current) { + set({value, loading: false}); + } + }, + error => { + if (mounted.current) { + set({error, loading: false}); + } + } + ); + }, deps); + + return [state, callback] +}; + +export default useAsyncCallback;