Dans le développement frontend moderne, la distinction entre état serveur et état client n'est pas qu'un concept théorique : c'est la colonne vertébrale de l'architecture des applications. À mesure que la complexité des applications augmente, la manière dont nous gérons les données devient cruciale pour la performance, la maintenabilité et l'expérience utilisateur. Que vous travailliez avec React, Vue ou un framework hybride, comprendre quand récupérer des données depuis le serveur et quand les stocker localement est la clé pour construire des logiciels évolutifs.
Définir les deux états
Pour construire des applications efficaces, nous devons d'abord définir clairement ce qui constitue chaque type d'état. Cette distinction dicte les outils et les patterns que nous devons employer.
L'état serveur fait référence aux données qui proviennent du backend. Cela inclut les profils utilisateurs, les listes de produits, les jetons d'authentification et toute donnée nécessitant un appel API pour être récupérée ou modifiée. Ses caractéristiques principales sont :
- Latence : Elle n'est pas locale, ce qui implique des requêtes réseau.
- Mise en cache : Elle doit être mise en cache pour éviter des rechargements inutiles.
- Synchronisation : Elle peut devoir être synchronisée entre plusieurs onglets ou appareils.
- Propriété : La source unique de vérité réside sur le serveur.
L'état client fait référence aux données qui existent dans le navigateur et qui ne nécessitent pas nécessairement un appel API. Cela inclut les états de l'interface utilisateur tels que les identifiants d'onglets actifs, les toggles d'ouverture/fermeture de modales, les valeurs des champs de formulaire et les préférences de thème. Ses caractéristiques principales sont :
- Latence : Elle est locale, donc l'accès est immédiat.
- Synchronisation : Elle n'a pas besoin d'être synchronisée avec un serveur backend.
- Propriété : L'état est éphémère et lié à la session en cours.
Le piège de l'utilisation de l'état global pour les données serveur
Un anti-pattern courant, en particulier chez les développeurs qui migrent de Redux vers des solutions plus récentes, consiste à traiter l'état serveur comme un état client global. L'utilisation d'outils comme Redux, Vuex ou Pinia pour mettre en cache les réponses API conduit souvent à du code redondant. Vous vous retrouvez à écrire du code boilerplate pour les états de chargement, la gestion des erreurs et l'invalidation du cache, alors que ces fonctionnalités existent déjà dans des bibliothèques spécialisées.
Par exemple, considérez un composant affichant une liste d'utilisateurs. Si vous stockez cette liste dans l'état client global, vous devez gérer manuellement le cycle de vie asynchrone :
// Anti-pattern : Gestion manuelle côté client de l'état serveur
useEffect(() => {
const fetchUsers = async () => {
setUsersLoading(true);
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (error) {
setUsersError(error);
} finally {
setUsersLoading(false);
}
};
fetchUsers();
}, []);
Cette approche ne passe pas bien à l'échelle. Si un autre composant a besoin des mêmes données utilisateur, vous devez soit dupliquer la logique, soit créer des sélecteurs complexes, ce qui entraîne du prop-drilling et un couplage fort.
La solution : des outils spécialisés pour l'état serveur
L'écosystème moderne propose des bibliothèques spécialisées conçues spécifiquement pour gérer l'état serveur. Dans l'écosystème React, React Query (TanStack Query) est devenu la norme. Dans l'écosystème Vue, Vue Query ou Orval associé aux API de composition natives jouent des rôles similaires.
Ces bibliothèques gèrent automatiquement la mise en cache, le rechargement en arrière-plan et la déduplication des requêtes. Voici à quoi pourrait ressembler un composant React utilisant React Query :
// Bonne pratique : Utilisation de React Query pour l'état serveur
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
if (isLoading) return <div>Chargement...</div>;
if (error) return <div>Une erreur s'est produite : {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Cette solution est déclarative, concise et gère automatiquement la mise en cache et la synchronisation.
Conclusion : Gardez-le simple, gardez-le local
La règle générale est simple : si les données proviennent du serveur, utilisez une bibliothèque de gestion d'état serveur. Si les données sont liées à l'interface utilisateur et restent dans le navigateur, utilisez l'état local du composant, le contexte ou un store client global comme Redux ou Pinia. En respectant les limites entre l'état serveur et l'état client, vous réduisez le code boilerplate, améliorez les performances et construisez des applications plus faciles à maintenir. Choisissez le bon outil pour la bonne tâche, et votre architecture vous en remerciera.