Skip to content

Commit

Permalink
feat(useAsyncFn): support lifecycle callbacks & improve render perfor…
Browse files Browse the repository at this point in the history
…mance
  • Loading branch information
vikiboss committed Aug 5, 2024
1 parent 538e482 commit fe1b821
Showing 1 changed file with 104 additions and 26 deletions.
130 changes: 104 additions & 26 deletions src/use-async-fn/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { useRef } from 'react'
import { useLatest } from '../use-latest'
import { useSetState } from '../use-set-state'
import { useRender } from '../use-render'
import { useStableFn } from '../use-stable-fn'
import { isFunction } from '../utils/basic'

import type { SetStateAction } from 'react'
import type { ReactSetState } from '../use-safe-state'
import type { AnyFunc } from '../utils/basic'

export interface UseAsyncFnOptions {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export interface UseAsyncFnOptions<D = any> {
/**
* Initial data to be used as the initial value
*
* @defaultValue undefined
*/
initialValue?: D
/**
* whether to clear the value before running the function
*
Expand All @@ -18,13 +28,39 @@ export interface UseAsyncFnOptions {
* @defaultValue undefined
*/
onError?: (error: unknown) => void
/**
* a function to run before the async function
*
* @defaultValue undefined
*/
onBefore?: () => void
/**
* a function to run after the async function
*
* @defaultValue undefined
*/
onSuccess?: (data: D) => void
/**
* a function to run after the async function
*
* @defaultValue undefined
*/
onFinally?: (data: D | undefined) => void
}

export interface UseAsyncFnReturns<T extends AnyFunc> {
export interface UseAsyncFnReturns<T extends AnyFunc, D = Awaited<ReturnType<T>>> {
/**
* a function to run the async function
*/
run: T
/**
* a function to cancel the async function
*/
cancel: () => void
/**
* manually set the value
*/
mutate: ReactSetState<D | undefined>
/**
* whether the async function is loading
*/
Expand All @@ -36,7 +72,7 @@ export interface UseAsyncFnReturns<T extends AnyFunc> {
/**
* the value returned by the async function
*/
value: Awaited<ReturnType<T>> | undefined
value: D | undefined
}

/**
Expand All @@ -45,46 +81,88 @@ export interface UseAsyncFnReturns<T extends AnyFunc> {
* @param {AnyFunc} fn - `AnyFunc`, the async function to run, see {@link AnyFunc}
* @returns {UseAsyncFnReturns} `UseAsyncFnReturns`, see {@link UseAsyncFnReturns}
*/
export function useAsyncFn<T extends AnyFunc>(fn: T, options: UseAsyncFnOptions = {}): UseAsyncFnReturns<T> {
const { clearBeforeRun, onError } = options
export function useAsyncFn<T extends AnyFunc, D = Awaited<ReturnType<T>>>(
fn: T,
options: UseAsyncFnOptions<D> = {},
): UseAsyncFnReturns<T, D> {
const { clearBeforeRun, onError, onBefore, onSuccess, onFinally } = options

const render = useRender()
const versionRef = useRef(0)

const [state, setState] = useSetState({
loading: false,
error: null as unknown | null,
value: undefined as Awaited<ReturnType<T>> | undefined,
const stateRef = useRef({
error: { used: false, value: false },
loading: { used: false, value: false },
value: { used: false, value: options.initialValue },
})

const latest = useLatest({ fn, onError, clear: clearBeforeRun })
function updateRefValue<T>(refItem: { used: boolean; value: T }, newValue: T, update = true) {
if (refItem.value === newValue) return
refItem.value = newValue
refItem.used && update && render()
}

function runWhenVersionMatch(version: number, fu: AnyFunc) {
version === versionRef.current && fu()
}

const stableRunFn = useStableFn(async (...args: Parameters<T>) => {
const ver = ++versionRef.current
const newState = latest.current.clear ? { loading: true, value: undefined } : { loading: true }
const cancel = useStableFn(() => {
versionRef.current++
updateRefValue(stateRef.current.loading, false)
})

setState(newState)
const latest = useLatest({ fn, onError, onBefore, onSuccess, onFinally, clearBeforeRun })

let error: unknown | null = null
let result: Awaited<ReturnType<T>> | undefined = undefined
const stableRunFn = useStableFn(async (...args: Parameters<T>) => {
const version = ++versionRef.current

let result: D | undefined = undefined

try {
result = await latest.current.fn(...args)
} catch (err) {
if (ver === versionRef.current) {
error = err
latest.current.onError?.(err)
latest.current.onBefore?.()

if (latest.current.clearBeforeRun) {
updateRefValue(stateRef.current.value, undefined, false)
}
updateRefValue(stateRef.current.loading, true)
result = await latest.current.fn(...args)
latest.current.onSuccess?.(result as D)
updateRefValue(stateRef.current.value, result)
} catch (error) {
runWhenVersionMatch(version, () => {
latest.current.onError?.(error)
updateRefValue(stateRef.current.error, error, false)
})
} finally {
if (ver === versionRef.current) {
setState({ value: result, error, loading: false })
}
runWhenVersionMatch(version, () => {
latest.current.onFinally?.(result)
updateRefValue(stateRef.current.loading, false, false)
})
}

return result
}) as T

const mutate = useStableFn((action: SetStateAction<D | undefined>) => {
const nextState = isFunction(action) ? action(stateRef.current.value.value) : action
updateRefValue(stateRef.current.value, nextState)
})

return {
run: stableRunFn,
...state,
mutate,
cancel,
get loading() {
stateRef.current.loading.used = true
return stateRef.current.loading.value
},
get error() {
stateRef.current.error.used = true
return stateRef.current.error.value
},
get value() {
stateRef.current.value.used = true
return stateRef.current.value.value
},
}
}

0 comments on commit fe1b821

Please sign in to comment.