diff --git a/.gitignore b/.gitignore index 523d37b356..0264808aec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ pids *.seed *.pid.lock +# IDE files +.idea +.vscode + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -71,4 +75,4 @@ src/parser.ts .puppet-master/ storybook-static/ -package-lock.json \ No newline at end of file +package-lock.json diff --git a/docs/useAsyncRetry.md b/docs/useAsyncRetry.md new file mode 100644 index 0000000000..ff2a9ac0b3 --- /dev/null +++ b/docs/useAsyncRetry.md @@ -0,0 +1,47 @@ +# `useAsyncRetry` + +Uses `useAsync` with an additional `retry` method to easily retry/refresh the async function; + + +## Usage + +```jsx +import {useAsyncRetry} from 'react-use'; + +// Returns a Promise that resolves after one second. +const fn = () => new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.5) { + reject(new Error('Random error!')); + } else { + resolve('RESOLVED'); + } + }, 1000); +}); + +const Demo = () => { + const state = useAsync(fn); + + return ( +
+ {state.loading? +
Loading...
+ : state.error? +
Error...
+ :
Value: {state.value}
+ } + {!state.loading? + state.retry()}>Retry + : null + } +
+ ); +}; +``` + + +## Reference + +```ts +useAsyncRetry(fn, args?: any[]); +``` diff --git a/src/__stories__/useAsyncRetry.story.tsx b/src/__stories__/useAsyncRetry.story.tsx new file mode 100644 index 0000000000..38b2985724 --- /dev/null +++ b/src/__stories__/useAsyncRetry.story.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {useAsyncRetry} from '..'; +import ShowDocs from '../util/ShowDocs'; + +const fnRetry = () => + new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.5) { + reject(new Error('Random error!')); + } else { + resolve('RESOLVED'); + } + }, 1000); + }); + +const DemoRetry = () => { + const { loading, value, error, retry } = useAsyncRetry(fnRetry); + + return ( +
+ {loading? +
Loading...
+ : error? +
Error: {error.message}
+ :
Value: {value}
+ } + retry()}>Retry +
+ ); +}; + +storiesOf('Side effects|useAsyncRetry', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/index.ts b/src/index.ts index 343a453352..66bdf580d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import createMemo from './createMemo'; import useAsync from './useAsync'; +import useAsyncRetry from './useAsyncRetry'; import useAudio from './useAudio'; import useBattery from './useBattery'; import useBoolean from './useBoolean'; @@ -52,6 +53,7 @@ import useWait from './useWait'; export { createMemo, useAsync, + useAsyncRetry, useAudio, useBattery, useBoolean, diff --git a/src/useAsync.ts b/src/useAsync.ts index f9f2e1cc43..8749baec8a 100644 --- a/src/useAsync.ts +++ b/src/useAsync.ts @@ -1,51 +1,42 @@ -import {useState, useEffect, useCallback} from 'react'; +import { useState, useEffect, useCallback } from 'react'; -export type AsyncState = -| { - loading: true; - error?: undefined; - value?: undefined; -} -| { - loading: false; - error: Error; - value?: undefined; -} -| { - loading: false; - error?: undefined; - value: T; +export interface AsyncState { + loading: boolean; + error?: Error; + value?: T; }; const useAsync = (fn: () => Promise, args?) => { const [state, set] = useState>({ - loading: true, + loading: true }); const memoized = useCallback(fn, args); useEffect(() => { let mounted = true; set({ - loading: true, + loading: true }); const promise = memoized(); - promise - .then(value => { + promise.then( + value => { if (mounted) { set({ loading: false, - value, + value }); } - }, error => { + }, + error => { if (mounted) { set({ loading: false, - error, + error }); } - }); + } + ); return () => { mounted = false; diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts new file mode 100644 index 0000000000..0382df244f --- /dev/null +++ b/src/useAsyncRetry.ts @@ -0,0 +1,22 @@ +import { useCallback, useState } from 'react'; +import useAsync from './useAsync'; + +const useAsyncRetry = (fn: () => Promise, args: any[] = []) => { + const [attempt, setAttempt] = useState(0); + const memoized = useCallback(() => fn(), [...args, attempt]); + const state = useAsync(memoized); + + const retry = useCallback(() => { + if (state.loading) { + if (process.env.NODE_ENV === 'development') { + console.log('You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.'); + } + return; + } + setAttempt(attempt + 1); + }, [memoized, state, attempt]); + + return { ...state, retry }; +}; + +export default useAsyncRetry;