import { useCallback, useEffect, useRef, useState } from 'react'; export interface PagedQueryState { items: T[]; isLoading: boolean; error: unknown; /** `true` tant que la dernière page reçue était pleine (d'autres pages possibles). */ hasMore: boolean; /** Charge la page suivante et l'ajoute aux items existants. */ loadMore: () => void; /** Réinitialise et recharge depuis la première page. */ reload: () => void; } /** * Pagination offset-based pour les endpoints `/query` inwink * (cf. docs/inwink-api/query.md). `loadPage(index, signal)` doit renvoyer le * tableau d'entités de la page demandée. La pagination étant stateless côté * serveur, des doublons peuvent survenir entre pages : les items sont * dédupliqués par `getId`. Les réponses obsolètes (démontage) sont ignorées. * * Généralise le motif déjà utilisé par les pickers (useContextOptions) à * n'importe quelle vue liste. */ export function useInwinkPagedQuery( loadPage: (pageIndex: number, signal: AbortSignal) => Promise, getId: (item: T) => string, options?: { pageSize?: number }, ): PagedQueryState { const pageSize = options?.pageSize ?? 50; const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(false); // `loadPage` / `getId` changent d'identité à chaque rendu : lus via ref. const loadRef = useRef(loadPage); loadRef.current = loadPage; const getIdRef = useRef(getId); getIdRef.current = getId; const pageIndex = useRef(0); const reqId = useRef(0); const controller = useRef(null); const fetchPage = useCallback( (index: number, append: boolean) => { const id = ++reqId.current; controller.current?.abort(); const ac = new AbortController(); controller.current = ac; setIsLoading(true); setError(null); loadRef .current(index, ac.signal) .then((page) => { if (id !== reqId.current) return; pageIndex.current = index; setHasMore(page.length >= pageSize); setItems((current) => { const base = append ? current : []; const seen = new Set(base.map((it) => getIdRef.current(it))); const merged = [...base]; for (const it of page) { const key = getIdRef.current(it); if (!seen.has(key)) { seen.add(key); merged.push(it); } } return merged; }); }) .catch((err: unknown) => { if (id === reqId.current && !ac.signal.aborted) setError(err); }) .finally(() => { if (id === reqId.current) setIsLoading(false); }); }, [pageSize], ); useEffect(() => { fetchPage(0, false); return () => { reqId.current++; controller.current?.abort(); }; }, [fetchPage]); const loadMore = useCallback(() => { if (!isLoading && hasMore) fetchPage(pageIndex.current + 1, true); }, [fetchPage, hasMore, isLoading]); const reload = useCallback(() => fetchPage(0, false), [fetchPage]); return { items, isLoading, error, hasMore, loadMore, reload }; }