import { useCallback, useEffect, useRef, useState } from 'react'; import type { DependencyList } from 'react'; export interface AsyncState { /** Données de la dernière exécution réussie, sinon `undefined`. */ data: T | undefined; /** Erreur de la dernière exécution échouée, sinon `null`. */ error: unknown; isLoading: boolean; /** Relance manuellement l'opération (ex. bouton « Réessayer »). */ refetch: () => void; } /** * Exécute une opération asynchrone (typiquement un appel `useInwink*Api`) et * expose son état `data` / `error` / `isLoading`, avec annulation via * `AbortController` et déduplication des réponses obsolètes : seule la dernière * exécution en vol met à jour l'état (pas de setState après démontage). * * `fn` reçoit un `AbortSignal` à propager à l'appel (`{ signal }` dans `init`). * L'opération est (re)lancée au montage et chaque fois que `deps` change. */ export function useAsync( fn: (signal: AbortSignal) => Promise, deps: DependencyList, ): AsyncState { const [data, setData] = useState(undefined); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); // `fn` change d'identité à chaque rendu : on le lit via une ref pour ne // dépendre que de `deps` (même motif que `loadRef` dans useContextOptions). const fnRef = useRef(fn); fnRef.current = fn; // Identifiant monotone de requête : seule la dernière en vol met à jour l'état. const reqId = useRef(0); const controller = useRef(null); const run = useCallback(() => { const id = ++reqId.current; controller.current?.abort(); const ac = new AbortController(); controller.current = ac; setIsLoading(true); setError(null); fnRef .current(ac.signal) .then((result) => { if (id === reqId.current) setData(result); }) .catch((err: unknown) => { if (id === reqId.current && !ac.signal.aborted) setError(err); }) .finally(() => { if (id === reqId.current) setIsLoading(false); }); }, []); useEffect(() => { run(); return () => { reqId.current++; controller.current?.abort(); }; // `run` est stable ; on relance uniquement quand les dépendances métier changent. // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); return { data, error, isLoading, refetch: run }; }