From 2d026126286980d6e5c5b8962260758257e6564e Mon Sep 17 00:00:00 2001 From: lvl99 Date: Sat, 16 Mar 2019 14:29:58 +0100 Subject: [PATCH 1/5] first pass at retry/refresh ability for useAsync --- src/__stories__/useAsync.story.tsx | 66 +++++++++++++++------- src/useAsync.ts | 91 ++++++++++++++++++++---------- 2 files changed, 107 insertions(+), 50 deletions(-) diff --git a/src/__stories__/useAsync.story.tsx b/src/__stories__/useAsync.story.tsx index 84ed0eaff4..9763f8d1bb 100644 --- a/src/__stories__/useAsync.story.tsx +++ b/src/__stories__/useAsync.story.tsx @@ -1,29 +1,55 @@ -import * as React from 'react'; -import {storiesOf} from '@storybook/react'; -import {useAsync} from '..'; -import ShowDocs from '../util/ShowDocs'; +import * as React from "react"; +import { storiesOf } from "@storybook/react"; +import { useAsync } from ".."; +import ShowDocs from "../util/ShowDocs"; -const fn = () => new Promise((resolve) => { - setTimeout(() => { - resolve('RESOLVED'); - }, 1000); -}); +const fn = () => + new Promise(resolve => { + setTimeout(() => { + resolve("RESOLVED"); + }, 1000); + }); const Demo = () => { - const {loading, value} = useAsync(fn); + const { loading, value } = useAsync(fn); return ( -
- {loading - ?
Loading...
- :
Value: {value}
+
{loading ?
Loading...
:
Value: {value}
}
+ ); +}; + +const fnRetry = () => + new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.2) { + reject(new Error("Random error!")); + } else { + resolve("RESOLVED"); } -
+ }, 1000); + }); + +const DemoRetry = () => { + const { loading, value, error, retry } = useAsync(fnRetry); + + return ( + <> +
+ {loading ?
Loading...
:
Value: {value || "?"}
} +
+ {error ? ( +
+

Errored: {error.message}

+ retry()}> + Retry? + +
+ ) : null} + ); }; -storiesOf('Side effects|useAsync', module) - .add('Docs', () => ) - .add('Demo', () => - - ) +storiesOf("Side effects|useAsync", module) + .add("Docs", () => ) + .add("Demo", () => ) + .add("Demo: retry", () => ); diff --git a/src/useAsync.ts b/src/useAsync.ts index f9f2e1cc43..2d4deb5aa5 100644 --- a/src/useAsync.ts +++ b/src/useAsync.ts @@ -1,58 +1,89 @@ -import {useState, useEffect, useCallback} from 'react'; +import { useState, useEffect, useCallback, useRef } from "react"; export type AsyncState = -| { - loading: true; - error?: undefined; - value?: undefined; + | { + loading: true; + error?: undefined; + value?: undefined; + } + | { + loading: false; + error: Error; + value?: undefined; + } + | { + loading: false; + error?: undefined; + value: T; + }; + +interface AsyncStateObject { + loading: boolean; + error?: Error; + value?: T; } -| { - loading: false; - error: Error; - value?: undefined; + +interface AsyncRef { + mounted: boolean; + busy: boolean; + attempt: number; } -| { - loading: false; - error?: undefined; - value: T; -}; const useAsync = (fn: () => Promise, args?) => { - const [state, set] = useState>({ - loading: true, + const ref = useRef({ mounted: false, attempt: 0, busy: false }); + const [state, set] = useState>({ + loading: false }); + const memoized = useCallback(fn, args); - useEffect(() => { - let mounted = true; + const attemptAsync = useCallback(() => { + // Abort new attempt if already busy + if (ref.current.busy) { + console.log("useAsync is currently busy, please wait!"); + return; + } + + ref.current.busy = true; + ref.current.attempt = ref.current.attempt + 1; + set({ - loading: true, + loading: true }); - const promise = memoized(); - promise - .then(value => { - if (mounted) { + memoized().then( + value => { + if (ref.current.mounted) { + ref.current.busy = false; set({ loading: false, - value, + value }); } - }, error => { - if (mounted) { + }, + error => { + if (ref.current.mounted) { + ref.current.busy = false; set({ loading: false, - error, + error }); } - }); + } + ); + }, [memoized]); + + useEffect(() => { + ref.current.mounted = true; + + attemptAsync(); return () => { - mounted = false; + ref.current.mounted = false; }; }, [memoized]); - return state; + return { ...state, retry: attemptAsync }; }; export default useAsync; From 8b7d5ed3acb8eb2d7049866a8a144f287d9cf0d9 Mon Sep 17 00:00:00 2001 From: lvl99 Date: Wed, 20 Mar 2019 15:12:45 +0100 Subject: [PATCH 2/5] reverted useAsync and created new hook useAsyncRetry which leverages useAsync added story and docs for useAsyncRetry fixed up new code formatting to match project --- .gitignore | 6 ++- docs/useAsyncRetry.md | 47 ++++++++++++++++++ src/__stories__/useAsync.story.tsx | 66 ++++++++----------------- src/__stories__/useAsyncRetry.story.tsx | 35 +++++++++++++ src/index.ts | 2 + src/useAsync.ts | 66 +++++-------------------- src/useAsyncRetry.ts | 19 +++++++ 7 files changed, 141 insertions(+), 100 deletions(-) create mode 100644 docs/useAsyncRetry.md create mode 100644 src/__stories__/useAsyncRetry.story.tsx create mode 100644 src/useAsyncRetry.ts 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__/useAsync.story.tsx b/src/__stories__/useAsync.story.tsx index 9763f8d1bb..84ed0eaff4 100644 --- a/src/__stories__/useAsync.story.tsx +++ b/src/__stories__/useAsync.story.tsx @@ -1,55 +1,29 @@ -import * as React from "react"; -import { storiesOf } from "@storybook/react"; -import { useAsync } from ".."; -import ShowDocs from "../util/ShowDocs"; +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {useAsync} from '..'; +import ShowDocs from '../util/ShowDocs'; -const fn = () => - new Promise(resolve => { - setTimeout(() => { - resolve("RESOLVED"); - }, 1000); - }); +const fn = () => new Promise((resolve) => { + setTimeout(() => { + resolve('RESOLVED'); + }, 1000); +}); const Demo = () => { - const { loading, value } = useAsync(fn); + const {loading, value} = useAsync(fn); return ( -
{loading ?
Loading...
:
Value: {value}
}
- ); -}; - -const fnRetry = () => - new Promise((resolve, reject) => { - setTimeout(() => { - if (Math.random() > 0.2) { - reject(new Error("Random error!")); - } else { - resolve("RESOLVED"); +
+ {loading + ?
Loading...
+ :
Value: {value}
} - }, 1000); - }); - -const DemoRetry = () => { - const { loading, value, error, retry } = useAsync(fnRetry); - - return ( - <> -
- {loading ?
Loading...
:
Value: {value || "?"}
} -
- {error ? ( -
-

Errored: {error.message}

- retry()}> - Retry? - -
- ) : null} - +
); }; -storiesOf("Side effects|useAsync", module) - .add("Docs", () => ) - .add("Demo", () => ) - .add("Demo: retry", () => ); +storiesOf('Side effects|useAsync', module) + .add('Docs', () => ) + .add('Demo', () => + + ) 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 7e5dd099b7..8a703f96ab 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'; @@ -51,6 +52,7 @@ import useWait from './useWait'; export { createMemo, useAsync, + useAsyncRetry, useAudio, useBattery, useBoolean, diff --git a/src/useAsync.ts b/src/useAsync.ts index 2d4deb5aa5..8749baec8a 100644 --- a/src/useAsync.ts +++ b/src/useAsync.ts @@ -1,60 +1,27 @@ -import { useState, useEffect, useCallback, useRef } 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; - }; - -interface AsyncStateObject { +export interface AsyncState { loading: boolean; error?: Error; value?: T; -} - -interface AsyncRef { - mounted: boolean; - busy: boolean; - attempt: number; -} +}; const useAsync = (fn: () => Promise, args?) => { - const ref = useRef({ mounted: false, attempt: 0, busy: false }); - const [state, set] = useState>({ - loading: false + const [state, set] = useState>({ + loading: true }); - const memoized = useCallback(fn, args); - const attemptAsync = useCallback(() => { - // Abort new attempt if already busy - if (ref.current.busy) { - console.log("useAsync is currently busy, please wait!"); - return; - } - - ref.current.busy = true; - ref.current.attempt = ref.current.attempt + 1; - + useEffect(() => { + let mounted = true; set({ loading: true }); + const promise = memoized(); - memoized().then( + promise.then( value => { - if (ref.current.mounted) { - ref.current.busy = false; + if (mounted) { set({ loading: false, value @@ -62,8 +29,7 @@ const useAsync = (fn: () => Promise, args?) => { } }, error => { - if (ref.current.mounted) { - ref.current.busy = false; + if (mounted) { set({ loading: false, error @@ -71,19 +37,13 @@ const useAsync = (fn: () => Promise, args?) => { } } ); - }, [memoized]); - - useEffect(() => { - ref.current.mounted = true; - - attemptAsync(); return () => { - ref.current.mounted = false; + mounted = false; }; }, [memoized]); - return { ...state, retry: attemptAsync }; + return state; }; export default useAsync; diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts new file mode 100644 index 0000000000..688c6dc66b --- /dev/null +++ b/src/useAsyncRetry.ts @@ -0,0 +1,19 @@ +import { useCallback, useState } from 'react'; +import useAsync from './useAsync'; + +const useAsyncRetry = (fn: () => Promise, args: any[] = []) => { + const [attempt, setAttempt] = useState(0); + const memoized = useCallback(async () => await fn(), [...args, attempt]); + const state = useAsync(memoized); + + const retry = useCallback(() => { + if (state.loading) { + return; + } + setAttempt(attempt + 1); + }, [memoized, state, attempt]); + + return { ...state, retry }; +}; + +export default useAsyncRetry; From 7ceff990aac542bae215c15b284f5c9ebdd09838 Mon Sep 17 00:00:00 2001 From: lvl99 Date: Wed, 20 Mar 2019 15:44:55 +0100 Subject: [PATCH 3/5] added console message to notify user about useAsyncRetry operation still loading --- src/useAsyncRetry.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts index 688c6dc66b..db408267c5 100644 --- a/src/useAsyncRetry.ts +++ b/src/useAsyncRetry.ts @@ -8,6 +8,9 @@ const useAsyncRetry = (fn: () => Promise, args: any[] = []) => { 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); From 46d0c2766b21daafe8038790b82da1d1570693a9 Mon Sep 17 00:00:00 2001 From: lvl99 Date: Wed, 20 Mar 2019 17:06:36 +0100 Subject: [PATCH 4/5] correct incorrect logic --- src/useAsyncRetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts index db408267c5..18b54a3ced 100644 --- a/src/useAsyncRetry.ts +++ b/src/useAsyncRetry.ts @@ -8,7 +8,7 @@ const useAsyncRetry = (fn: () => Promise, args: any[] = []) => { const retry = useCallback(() => { if (state.loading) { - if (process.env.NODE_ENV !== 'development') { + 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; From 8e3de1cdf1e287ada1f7baf19679fdf85abcc8a8 Mon Sep 17 00:00:00 2001 From: Va Da Date: Thu, 21 Mar 2019 10:15:27 +0100 Subject: [PATCH 5/5] perf: remove asyn/await wrapper --- src/useAsyncRetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts index 18b54a3ced..0382df244f 100644 --- a/src/useAsyncRetry.ts +++ b/src/useAsyncRetry.ts @@ -3,7 +3,7 @@ import useAsync from './useAsync'; const useAsyncRetry = (fn: () => Promise, args: any[] = []) => { const [attempt, setAttempt] = useState(0); - const memoized = useCallback(async () => await fn(), [...args, attempt]); + const memoized = useCallback(() => fn(), [...args, attempt]); const state = useAsync(memoized); const retry = useCallback(() => {