Skip to content

Commit 7f3bf48

Browse files
feat: add useAsync, useAsyncFunc, useAsyncRetry
1 parent f1dc5d4 commit 7f3bf48

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

.changeset/angry-hairs-play.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@yamada-ui/utils': patch
3+
---
4+
5+
Added useAsync, useAsyncFunc, useAsyncRetry.

packages/utils/src/react.tsx

+102
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,105 @@ export const useUpdateEffect = (callback: React.EffectCallback, deps: React.Depe
195195
}
196196
}, [])
197197
}
198+
199+
export type FunctionReturningPromise = (...args: any[]) => Promise<any>
200+
201+
export const useAsync = <T extends FunctionReturningPromise>(
202+
func: T,
203+
deps: React.DependencyList = [],
204+
) => {
205+
const [state, callback] = useAsyncFunc(func, deps, { loading: true })
206+
207+
React.useEffect(() => {
208+
callback()
209+
}, [callback])
210+
211+
return state
212+
}
213+
214+
export type AsyncState<T> =
215+
| {
216+
loading: boolean
217+
error?: undefined
218+
value?: undefined
219+
}
220+
| {
221+
loading: true
222+
error?: Error | undefined
223+
value?: T
224+
}
225+
| {
226+
loading: false
227+
error: Error
228+
value?: undefined
229+
}
230+
| {
231+
loading: false
232+
error?: undefined
233+
value: T
234+
}
235+
236+
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T> ? T : never
237+
238+
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> = AsyncState<
239+
PromiseType<ReturnType<T>>
240+
>
241+
242+
export type AsyncFnReturn<T extends FunctionReturningPromise = FunctionReturningPromise> = [
243+
StateFromFunctionReturningPromise<T>,
244+
T,
245+
]
246+
247+
export const useAsyncFunc = <T extends FunctionReturningPromise>(
248+
func: T,
249+
deps: React.DependencyList = [],
250+
initialState: StateFromFunctionReturningPromise<T> = { loading: false },
251+
): AsyncFnReturn<T> => {
252+
const lastCallId = React.useRef(0)
253+
const isMounted = useIsMounted()
254+
const [state, setState] = React.useState<StateFromFunctionReturningPromise<T>>(initialState)
255+
256+
const callback = React.useCallback((...args: Parameters<T>): ReturnType<T> => {
257+
const callId = ++lastCallId.current
258+
259+
if (!state.loading) {
260+
setState((prevState) => ({ ...prevState, loading: true }))
261+
}
262+
263+
return func(...args).then(
264+
(value) => {
265+
if (isMounted.current && callId === lastCallId.current) setState({ value, loading: false })
266+
267+
return value
268+
},
269+
(error) => {
270+
if (isMounted.current && callId === lastCallId.current) setState({ error, loading: false })
271+
272+
return error
273+
},
274+
) as ReturnType<T>
275+
// eslint-disable-next-line react-hooks/exhaustive-deps
276+
}, deps)
277+
278+
return [state, callback as unknown as T]
279+
}
280+
281+
export type AsyncStateRetry<T> = AsyncState<T> & {
282+
retry(): void
283+
}
284+
285+
export const useAsyncRetry = <T,>(func: () => Promise<T>, deps: React.DependencyList = []) => {
286+
const [attempt, setAttempt] = React.useState<number>(0)
287+
const state = useAsync(func, [...deps, attempt])
288+
289+
const stateLoading = state.loading
290+
291+
const retry = React.useCallback(() => {
292+
if (stateLoading) return
293+
294+
setAttempt((currentAttempt) => currentAttempt + 1)
295+
// eslint-disable-next-line react-hooks/exhaustive-deps
296+
}, [...deps, stateLoading])
297+
298+
return { ...state, retry }
299+
}

0 commit comments

Comments
 (0)