initial commit
This commit is contained in:
1 parent
5b935301e5
commit
85453852d5
105 files changed
+24846
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export interface ApiState<T> {
|
||||
data: T | null
|
||||
error: string | null
|
||||
loading: boolean
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
function message(err: unknown): string {
|
||||
if (err instanceof Error) return err.message
|
||||
if (typeof err === 'string') return err
|
||||
return 'request failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `fn` on mount and whenever `deps` change, aborting the previous request
|
||||
* so a fast filter change cannot let a slow earlier response win a race.
|
||||
*/
|
||||
export function useApi<T>(fn: () => Promise<T>, deps: unknown[] = []): ApiState<T> {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [nonce, setNonce] = useState(0)
|
||||
|
||||
const fnRef = useRef(fn)
|
||||
fnRef.current = fn
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
let live = true
|
||||
setLoading(true)
|
||||
|
||||
fnRef
|
||||
.current()
|
||||
.then((value) => {
|
||||
if (!live) return
|
||||
setData(value)
|
||||
setError(null)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!live) return
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
setError(message(err))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!live) return
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
live = false
|
||||
controller.abort()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps, nonce])
|
||||
|
||||
const refetch = useCallback(() => setNonce((n) => n + 1), [])
|
||||
|
||||
return { data, error, loading, refetch }
|
||||
}
|
||||
|
||||
export interface AsyncAction<TArgs extends unknown[], T> {
|
||||
run: (...args: TArgs) => Promise<T | null>
|
||||
pending: boolean
|
||||
error: string | null
|
||||
clearError: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A fire-and-forget mutation: `run` never rejects, it resolves to `null` on
|
||||
* failure and puts the message in `error` so the caller can just toast it.
|
||||
*/
|
||||
export function useAsyncAction<TArgs extends unknown[], T>(
|
||||
fn: (...args: TArgs) => Promise<T>,
|
||||
): AsyncAction<TArgs, T> {
|
||||
const [pending, setPending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const mounted = useRef(true)
|
||||
const fnRef = useRef(fn)
|
||||
fnRef.current = fn
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true
|
||||
return () => {
|
||||
mounted.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const run = useCallback(async (...args: TArgs): Promise<T | null> => {
|
||||
setPending(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await fnRef.current(...args)
|
||||
return result
|
||||
} catch (err) {
|
||||
if (mounted.current) setError(message(err))
|
||||
return null
|
||||
} finally {
|
||||
if (mounted.current) setPending(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearError = useCallback(() => setError(null), [])
|
||||
|
||||
return { run, pending, error, clearError }
|
||||
}
|
||||
Reference in new issue
Block a user