Skip to content

Comment les données circulent fr FR

rocambille edited this page Jun 4, 2026 · 1 revision

Résumé : Cette page explique le cycle complet de circulation des données dans StartER, de la récupération des données à leur modification et au rafraîchissement de l'interface.

Vue d'ensemble

StartER utilise une approche légère et native pour la gestion des données. Il n'y a aucune bibliothèque externe de gestion d'état (Redux, Zustand, etc.). Les données circulent à travers un cycle de fetch → cache → rendu → mutation → invalidation → re-fetch.

Les trois fichiers clés sont :

  • src/react/helpers/cache.ts — lecture des données
  • src/react/helpers/mutate.ts — écriture des données
  • src/react/components/DataRefreshContext.tsx — pont entre les mutations et les re-rendus

Le cycle de circulation des données

Lecture

┌──────────────────────────────────────────────────────────────────┐
│                        Composant React                           │
│                                                                  │
│   const items = use(cache<Item[]>("/api/items"));                │
│                         │                                        │
│                         ▼                                        │
│              ┌─────────────────────┐                             │
│              │  cache("/api/...")  │──── Cache HIT ──→ return    │
│              └─────────────────────┘                             │
│                         │ Cache MISS                             │
│                         ▼                                        │
│              ┌─────────────────────┐                             │
│              │  fetch("/api/...")  │──→ API Express ──→ SQLite   │
│              └─────────────────────┘                             │
│                         │                                        │
│                         ▼                                        │
│                    Stocker en cache                              │
│                    Rendre le composant                           │
└──────────────────────────────────────────────────────────────────┘

Écriture

                   L'utilisateur déclenche
                       une mutation
                            │
                            ▼
┌──────────────────────────────────────────────────────────────────┐
│                        useMutate()                               │
│                                                                  │
│   1. apiMutate() ──→ fetch(url, { method, body })                │
│      └── Inclut le jeton CSRF (cookie + header)                  │
│                                                                  │
│   2. invalidateCache(["/api/items"]) ──→ supprime les entrées    │
│                                                                  │
│   3. refresh() ──→ incrémente le tick de DataRefreshContext      │
│      └── Les composants utilisant les chemins invalidés          │
│         se re-suspendent                                         │
│         └── React appelle use(cache(...)) à nouveau              │
│            └── Cache MISS → nouveau fetch → re-rendu             │
└──────────────────────────────────────────────────────────────────┘

Lecture des données en détail

cache(path)

Le helper cache (src/react/helpers/cache.ts) stocke les réponses API sous forme de Promesses dans une Map en mémoire. Lorsqu'un composant appelle cache("/api/items") :

  1. Cache hit : retourne immédiatement la Promesse stockée (aucun appel réseau).
  2. Cache miss : crée une Promesse fetch(), la stocke dans la Map, et la retourne.
export const cache = <T extends Json>(url: string): Promise<T> => {
  if (!cacheData.has(url)) {
    cacheData.set(
      url,
      fetch(url).then<T>((response) => {
        if (!response.ok) {
          throw new Error(`${response.status}: ${response.statusText}`);
        }
        return response.json();
      }),
    );
  }

  return cacheData.get(url) as Promise<T>;
};

Note

Le cache stocke des Promesses, pas des valeurs résolues. C'est un prérequis du hook use de React, qui s'appuie sur l'identité de la Promesse pour gérer la suspension correctement.

use(promise)

Le hook use() de React 19 suspend le composant jusqu'à ce que la Promesse soit résolue. Combiné avec cache(), cela signifie :

  • Premier rendu : le composant se suspend → le fetch se déclenche → les données arrivent → le composant se rend.
  • Rendus suivants : cache hit → pas de suspension → rendu instantané.
import { use } from "react";
import { cache } from "../../helpers/cache";

function ItemList() {
  const items = use(cache<Item[]>("/api/items"));

  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.title}</li>)}
    </ul>
  );
}

Écriture des données en détail

useMutate()

Le hook useMutate (src/react/helpers/mutate.ts) orchestre trois étapes en séquence :

  1. Appel API via apiMutate() : envoie la requête de mutation (POST, PUT ou DELETE) avec attachement automatique du jeton CSRF.
  2. Invalidation du cache via invalidateCache() : supprime les chemins spécifiés de la Map en mémoire pour que la prochaine lecture déclenche un nouveau fetch.
  3. Signal de rafraîchissement via refresh() : incrémente un compteur global (DataRefreshContext) qui force tous les composants concernés à se re-suspendre et re-fetcher.
export function useMutate() {
  const { refresh } = useRefresh();

  return async (
    url: string,
    method: "post" | "put" | "delete",
    body?: unknown,
    invalidatePaths: string[] = [],
  ) => {
    const response = await apiMutate(url, method, body);

    for (const path of invalidatePaths) {
      invalidateCache(path);
    }
    refresh();

    return response;
  };
}

Exemple : créer un item

const mutate = useMutate();
const navigate = useNavigate();

const addItem = async (partialItem: Omit<Item, "id" | "user_id">) => {
  await mutate("/api/items", "post", partialItem, ["/api/items"]);

  navigate("/items");
};

Après l'exécution de mutate() :

  1. La requête POST a été envoyée et a réussi.
  2. La Promesse en cache pour "/api/items" a été supprimée.
  3. DataRefreshContext.tick a été incrémenté.
  4. Tout composant appelant use(cache<Item[]>("/api/items")) se re-suspendra, déclenchant un nouveau GET et un re-rendu avec la liste mise à jour.

DataRefreshContext

Pour que ce système fonctionne, l'application doit être enveloppée dans un <DataRefreshProvider>. Ce dernier est déjà configuré dans src/react/routes.tsx.

Il fournit un mécanisme de « signaling global » : dès qu'une mutation réussit, tous les composants utilisant des données en cache (et invalidées) se mettront à jour automatiquement pour afficher les données les plus récentes.

invalidateCache(basePath)

La fonction invalidateCache supporte deux modes :

  • Préfixe de chemin : invalidateCache("/api/items") supprime toutes les entrées dont la clé commence par "/api/items" (incluant "/api/items/1", "/api/items/2", etc.).
  • Wildcard : invalidateCache("*") vide l'intégralité du cache.

Bonnes pratiques

  • Toujours spécifier les chemins d'invalidation : oublier d'invalider signifie que l'utilisateur verra des données obsolètes après une mutation.
  • Utiliser invalidateCache("*") avec parcimonie : cela vide l'intégralité du cache et force tous les composants à re-fetcher.
  • Garder les composants atomiques : chaque composant devrait posséder son propre appel use(cache(...)) plutôt que de propager les données via des chaînes de props profondes.
  • Invalidation explicite plutôt qu'implicite : il est préférable de lister chaque chemin impacté (par exemple, ["/api/items", "/api/items/1"]) plutôt que de recourir à l'invalidation par wildcard.

Voir aussi

Clone this wiki locally