mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Review estrutural + qualidade (sem P1): spec ganha 3 pins do contrato mais sutil (loading em voo, retry pós-falha, quirk gate→not-ready com resposta stale — comportamento herdado dos 3 originais, preservado); UseAsyncSearchOptionsResult vira tipo local (export especulativo, knip types:error); docstring documenta o quirk e o trim; plano registra o ajuste de execução.
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
|
|
export const ASYNC_SEARCH_DEBOUNCE_MS = 250
|
|
|
|
type UseAsyncSearchOptionsResult<T> = {
|
|
options: T[]
|
|
loading: boolean
|
|
failed: boolean
|
|
}
|
|
|
|
type UseAsyncSearchOptionsArgs<T> = {
|
|
open: boolean
|
|
query: string
|
|
search: (query: string) => Promise<T[]>
|
|
isQueryReady?: (query: string) => boolean
|
|
}
|
|
|
|
/**
|
|
* E1 — the shared async-search-with-debounce effect (requestId guard against
|
|
* out-of-order responses, loading/failed states, per-consumer query gate).
|
|
* The server action is injected as a prop by the consumer, keeping this module
|
|
* client-safe; each consumer keeps its own filtering/grouping/pinned options.
|
|
* Contract quirks, preserved from the three original copies: a not-ready query
|
|
* resets options/loading/failed without invalidating an in-flight request
|
|
* (a stale response still lands after the reset), and the search receives the
|
|
* trimmed query.
|
|
*/
|
|
export function useAsyncSearchOptions<T extends { id: number }>({
|
|
open,
|
|
query,
|
|
search,
|
|
isQueryReady = () => true,
|
|
}: UseAsyncSearchOptionsArgs<T>): UseAsyncSearchOptionsResult<T> {
|
|
const [options, setOptions] = useState<T[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [failed, setFailed] = useState(false)
|
|
const requestId = useRef(0)
|
|
const searchRef = useRef(search)
|
|
const isQueryReadyRef = useRef(isQueryReady)
|
|
|
|
searchRef.current = search
|
|
isQueryReadyRef.current = isQueryReady
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
if (!isQueryReadyRef.current(query)) {
|
|
setOptions([])
|
|
setLoading(false)
|
|
setFailed(false)
|
|
return
|
|
}
|
|
|
|
const currentRequestId = ++requestId.current
|
|
const timeout = window.setTimeout(() => {
|
|
setLoading(true)
|
|
setFailed(false)
|
|
void searchRef
|
|
.current(query.trim())
|
|
.then((nextOptions) => {
|
|
if (requestId.current !== currentRequestId) return
|
|
setOptions(nextOptions)
|
|
})
|
|
.catch(() => {
|
|
if (requestId.current !== currentRequestId) return
|
|
setFailed(true)
|
|
})
|
|
.finally(() => {
|
|
if (requestId.current === currentRequestId) setLoading(false)
|
|
})
|
|
}, ASYNC_SEARCH_DEBOUNCE_MS)
|
|
|
|
return () => window.clearTimeout(timeout)
|
|
}, [open, query])
|
|
|
|
return { options, loading, failed }
|
|
}
|