initial commit

This commit is contained in:
developer committed 2026-09-27 11:20:34 +00:00
1 parent 5b935301e5
commit 85453852d5
105 files changed
+24846

No files matched your search

+107
View File
@@ -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 }
}