Composant DataList
Liste générique lazy-loading avec toggle list/card et persistance.
<DataList> (components/ui/data-list.tsx) est utilisé par toutes les
listes du projet. Une seule abstraction qui gère pagination cursor,
lazy-loading, switch list/card, persistance et reset.
Usage minimal
"use client";
import { DataList } from "@/components/ui/data-list";
export function AuditList() {
return (
<DataList
storageKey="audit-list"
loadPage={async ({ cursor, signal }) => {
const res = await fetch(`/api/admin/audit?cursor=${cursor ?? ""}`, { signal });
const json = await res.json();
return { items: json.items, nextCursor: json.nextCursor };
}}
getKey={(item) => item.id}
columns={[{ label: "Action" }, { label: "User" }, { label: "Date" }]}
renderRow={(item) => [
<>{item.action}</>,
<>{item.user?.name}</>,
<>{new Date(item.createdAt).toLocaleString()}</>,
]}
renderCard={(item) => (
<Card>
<CardHeader>{item.action}</CardHeader>
<CardContent>{item.user?.name}</CardContent>
</Card>
)}
emptyState={<p>Aucun log pour le moment.</p>}
/>
);
}Props
| Prop | Type | Rôle |
|---|---|---|
storageKey | string | Clé localStorage pour la vue préférée |
loadPage | ({ cursor, signal }) => Promise<{ items, nextCursor }> | Loader paginé, signal pour AbortController |
getKey | (item) => string | ID unique pour les keys React |
columns | { label: string }[] | En-têtes de la vue list |
renderRow | (item) => ReactNode[] | Cellules par ligne (1 par colonne) |
renderCard | (item) => ReactNode | Rendu vue card |
emptyState | ReactNode | Affiché quand 0 résultat |
errorState | ReactNode | Affiché en cas d'erreur loadPage |
defaultView | "list" | "card" | "auto" | auto = mobile card / desktop list |
hideToggle | boolean | Force une seule vue (pas de switch UI) |
resetKey | string | Reset complet quand cette valeur change (ex: filtres) |
Lazy loading
IntersectionObserver détecte le bas de la liste et appelle
loadPage({ cursor }) avec le cursor retourné précédemment. Pas de bouton
"Load more" — scroll infini natif.
L'AbortController est géré : si l'user change de filtre pendant un fetch,
le fetch en cours est annulé via signal.
Persistance localStorage
Clé : __view:${storageKey}. Stocke "list" ou "card". Restaurée au
mount. Si defaultView = "auto", la persistance ne s'applique qu'après
toggle manuel — sinon on respecte la breakpoint.
resetKey — pattern filtres
Quand l'utilisateur change un filtre, il faut réinitialiser le cursor et purger la liste. Au lieu de gérer ça à la main :
const [filters, setFilters] = useState({ role: "ALL", search: "" });
<DataList
resetKey={JSON.stringify(filters)}
loadPage={({ cursor, signal }) =>
fetch(`/api/users?cursor=${cursor}&role=${filters.role}&q=${filters.search}`, { signal })
.then(r => r.json())
}
// ...
/>À chaque changement de filters, le composant reset son state interne et
relance le premier loadPage.
Exemples concrets dans le projet
| Page | storageKey | Specifique |
|---|---|---|
/admin/audit | audit-list | Filtre action |
/admin/users | users-list | search + role filter + resetKey |
/account/api-keys | api-keys-list | reloadKey après create/revoke |
/account/organizations | orgs-list | defaultView="card" |
Allez plus loin
- Dashboard admin — où la liste est utilisée
- Audit log — usage typique
- Architecture —
components/ui/