Stale-While-Revalidate
Le problème que SWR résout
La récupération de données traditionnelle présente un arbitrage :
- Sans cache : les données sont toujours à jour, mais chaque navigation déclenche une requête réseau et affiche un spinner.
- Cache permanent : les réponses sont instantanées, mais les données deviennent périmées et l'UI reflète des informations obsolètes.
SWR (Stale-While-Revalidate) résout cet arbitrage.
Fonctionnement
Quand fetchQuery ou watchQuery est appelé avec des données périmées en cache :
- Retourner les données périmées immédiatement. L'UI s'affiche sans spinner.
- Déclencher une revalidation en arrière-plan. Le fetcher s'exécute silencieusement.
- Mettre à jour le cache et tous les streams actifs quand les données fraîches arrivent.
t=0:00 Success(user: Alice, updatedAt: 10:00) ← staleTime : 5 min
t=5:01 fetchQuery appelé
├── retourne Alice immédiatement (données périmées)
└── déclenche un fetch en arrière-plan
t=5:02 Success(user: Alice Martin, updatedAt: 10:05) ← mis à jour
└── les streams watchQuery émettent le nouvel état
Configuration du stale time
// Défaut : Duration.zero : toujours périmé, revalide toujours
// (sert quand même les données en cache instantanément à chaque appel)
options: QoraOptions(staleTime: Duration.zero),
// Frais pendant 5 minutes : pas d'appel réseau dans la fenêtre
options: QoraOptions(staleTime: Duration(minutes: 5)),
// Données de configuration : revalide uniquement toutes les heures
options: QoraOptions(staleTime: Duration(hours: 1)),
Choix du staleTime par type de données
| Type de données | staleTime recommandé |
|---|---|
| Prix en direct, notifications | Duration.zero |
| Profil utilisateur, paramètres | Duration(minutes: 5) |
| Contenu de post / article | Duration(minutes: 10) |
| Configuration qui change rarement | Duration(hours: 1) |
| Données de référence statiques | Duration(days: 1) |
SWR avec fetchQuery
Quand fetchQuery atteint une entrée de cache périmée, il retourne les données périmées de façon synchrone et déclenche une revalidation en arrière-plan. L'appel se résout immédiatement :
// Retourne immédiatement avec les données en cache si disponibles
final user = await client.fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(staleTime: Duration(minutes: 5)),
);
// Si les données étaient périmées :
// - `user` est la valeur périmée (retournée instantanément)
// - Un fetch en arrière-plan est déjà en cours
// - Les streams watchQuery actifs recevront l'utilisateur mis à jour quand le fetch se termine
SWR avec watchQuery
Les streams watchQuery sont l'interface principale pour le SWR. Les abonnés reçoivent le dernier état, y compris le résultat de la revalidation en arrière-plan :
StreamBuilder<QoraState<User>>(
stream: client.watchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(staleTime: Duration(minutes: 5)),
),
builder: (context, snapshot) {
return switch (snapshot.data) {
// Affiche les données périmées instantanément pendant que Loading(previousData: user) est émis
Loading(:final previousData) when previousData != null =>
Stack(children: [
UserCard(previousData),
const LinearProgressIndicator(),
]),
Success(:final data) => UserCard(data),
Failure(:final error, :final previousData) => Column(children: [
if (previousData != null) UserCard(previousData),
ErrorBanner('$error'),
]),
_ => const CircularProgressIndicator(),
};
},
)
Séquence d'états au montage avec cache périmé
Abonnement démarré
↓ émet immédiatement
Loading(previousData: Alice) ← données périmées visibles, indicateur de chargement discret
↓ fetch terminé
Success(data: Alice Martin) ← données fraîches, l'UI se met à jour sans rupture
Refetch au montage
Par défaut, watchQuery déclenche un fetch au premier abonnement quand les données sont périmées. Ce comportement est contrôlé par refetchOnMount :
// Toujours refetcher au montage, même si les données en cache sont encore fraîches
// (défaut quand QoraClientConfig.refetchOnMount est true)
options: QoraOptions(refetchOnMount: true),
// Fetch uniquement si les données sont absentes ou périmées ; ignorer le refetch si fraîches
options: QoraOptions(refetchOnMount: false),
// Hériter du paramètre global QoraClientConfig.refetchOnMount (défaut)
options: QoraOptions(refetchOnMount: null),
Polling (revalidation active)
Pour les données en direct, combiner le SWR avec refetchInterval pour rafraîchir continuellement le cache :
client.watchQuery<Stats>(
key: ['dashboard', 'stats'],
fetcher: api.getDashboardStats,
options: QoraOptions(
staleTime: Duration(seconds: 10),
refetchInterval: Duration(seconds: 30),
),
)
Le timer d'intervalle démarre quand le premier abonné s'attache, se déclenche toutes les refetchInterval, et s'arrête quand le dernier abonné annule. Le polling ne s'applique pas aux requêtes hors écran.
Forcer le rafraîchissement
Pour contourner la vérification de péremption et forcer une requête réseau immédiate, invalider d'abord la clé :
// Forcer un fetch frais au prochain accès
client.invalidate(['users', userId]);
// Ou précharger immédiatement des données fraîches
await client.prefetch<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(staleTime: Duration.zero),
);
Étapes suivantes
- Déduplication : Comment les appelants concurrents partagent une seule requête
- Cache : LRU, GC et
gcTimevsstaleTime - API QoraOptions : Référence complète des options
Cache
Le cache de Qora permet un accès instantané aux données. Apprenez comment les entrées de cache sont créées, vieillissent et sont collectées par le GC, comment le partage structurel évite les reconstructions inutiles, et comment contrôler chaque comportement.
Déduplication
Qora fusionne les requêtes concurrentes pour la même clé en un seul appel réseau, quel que soit le nombre d'appelants.