Documentation

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

PropTypeRôle
storageKeystringClé localStorage pour la vue préférée
loadPage({ cursor, signal }) => Promise<{ items, nextCursor }>Loader paginé, signal pour AbortController
getKey(item) => stringID 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) => ReactNodeRendu vue card
emptyStateReactNodeAffiché quand 0 résultat
errorStateReactNodeAffiché en cas d'erreur loadPage
defaultView"list" | "card" | "auto"auto = mobile card / desktop list
hideTogglebooleanForce une seule vue (pas de switch UI)
resetKeystringReset 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

PagestorageKeySpecifique
/admin/auditaudit-listFiltre action
/admin/usersusers-listsearch + role filter + resetKey
/account/api-keysapi-keys-listreloadKey après create/revoke
/account/organizationsorgs-listdefaultView="card"

Allez plus loin

On this page