-
Notifications
You must be signed in to change notification settings - Fork 7
Comment les données circulent fr FR
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.
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
┌──────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────────────────────┘
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 │
└──────────────────────────────────────────────────────────────────┘
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") :
- Cache hit : retourne immédiatement la Promesse stockée (aucun appel réseau).
-
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.
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>
);
}Le hook useMutate (src/react/helpers/mutate.ts) orchestre trois étapes en séquence :
-
Appel API via
apiMutate(): envoie la requête de mutation (POST, PUT ou DELETE) avec attachement automatique du jeton CSRF. -
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. -
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;
};
}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() :
- La requête POST a été envoyée et a réussi.
- La Promesse en cache pour
"/api/items"a été supprimée. -
DataRefreshContext.ticka été incrémenté. - 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.
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.
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.
- 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.
Co-création IA
Bien démarrer
Explications
Guides
Référence
Aller plus loin