Zustand vs Jotai : Choisir le bon gestionnaire d'état pour votre application React

Nouveau avec Zustand ou Jotai ? Consultez d’abord nos guides approfondis :
La gestion d’état React a considérablement évolué au-delà de la complexité de Redux. Pour les projets de petite à moyenne taille, des alternatives légères comme Zustand et Jotai ont gagné en popularité. Mais lequel devriez-vous choisir ? Cet article compare ces deux bibliothèques créées par le même développeur (Daishi Kato) pour vous aider à prendre une décision éclairée basée sur les besoins de votre projet.
Points clés à retenir
- Zustand utilise une approche centralisée, descendante, idéale pour l’état interconnecté et la collaboration en équipe
- Jotai utilise une approche atomique, ascendante, parfaite pour la réactivité fine et les données qui changent rapidement
- Les deux bibliothèques sont légères, performantes et compatibles TypeScript
- Zustand est souvent meilleur pour les applications plus importantes avec des relations d’état complexes
- Jotai excelle dans les scénarios nécessitant des éléments d’état indépendants avec un minimum de re-rendus
Les origines et la philosophie de Zustand et Jotai
Les deux bibliothèques ont été créées pour résoudre des problèmes spécifiques dans l’écosystème React, mais avec des approches différentes :
Zustand : L’approche descendante
Zustand (allemand pour “état”) a été publié en 2019 comme une alternative plus simple à Redux. Il suit une approche centralisée, descendante pour la gestion d’état.
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
Jotai : L’approche ascendante
Jotai (japonais pour “état”) a été publié en 2020 et s’inspire de Recoil. Il utilise une approche atomique, ascendante où l’état est divisé en petits atomes indépendants.
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
Modèles mentaux : Comment ils abordent l’état différemment
Comprendre le modèle mental derrière chaque bibliothèque est crucial pour choisir la bonne pour votre projet.
Le modèle basé sur le store de Zustand
Zustand utilise un store unique qui contient tout votre état et vos actions. Ce modèle est familier aux développeurs qui ont utilisé Redux :
// Création d'un store
const useUserStore = create((set) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async (id) => {
set({ isLoading: true });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, isLoading: false });
} catch (error) {
set({ error, isLoading: false });
}
}
}));
// Utilisation du store dans un composant
function Profile({ userId }) {
const { user, fetchUser } = useUserStore(
state => ({ user: state.user, fetchUser: state.fetchUser })
);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
return user ? <div>{user.name}</div> : <div>Chargement...</div>;
}
Le modèle atomique de Jotai
Jotai divise l’état en atomes qui peuvent être composés ensemble. C’est similaire au useState
de React mais avec la capacité de partager l’état entre les composants :
// Création d'atomes
const userAtom = atom(null);
const isLoadingAtom = atom(false);
const errorAtom = atom(null);
// Création d'un atome dérivé
const userStatusAtom = atom(
(get) => ({
user: get(userAtom),
isLoading: get(isLoadingAtom),
error: get(errorAtom)
})
);
// Création d'un atome d'action
const fetchUserAtom = atom(
null,
async (get, set, userId) => {
set(isLoadingAtom, true);
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set(userAtom, user);
set(isLoadingAtom, false);
} catch (error) {
set(errorAtom, error);
set(isLoadingAtom, false);
}
}
);
// Utilisation d'atomes dans un composant
function Profile({ userId }) {
const [{ user, isLoading }] = useAtom(userStatusAtom);
const [, fetchUser] = useAtom(fetchUserAtom);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
return user ? <div>{user.name}</div> : <div>Chargement...</div>;
}
Considérations de performance : Zustand vs Jotai
Les deux bibliothèques sont conçues pour être performantes, mais elles optimisent pour différents scénarios.
Profil de performance de Zustand
- Abonnements sélectifs : Les composants ne se re-rendent que lorsque leur état sélectionné change
- Taille du bundle : ~2,8 Ko minifié et gzippé
- Support des middleware : Middleware intégré pour l’optimisation des performances
- Mises à jour groupées : Groupe automatiquement les mises à jour d’état
Profil de performance de Jotai
- Mises à jour granulaires : Seuls les composants utilisant des atomes spécifiques se re-rendent
- Taille du bundle : ~3,5 Ko minifié et gzippé (package core)
- Optimisation au niveau des atomes : Contrôle fin sur quels changements d’état déclenchent des re-rendus
- État dérivé : Gère efficacement les valeurs calculées
Pour des données qui changent rapidement et n’affectent que des parties spécifiques de votre interface utilisateur, l’approche atomique de Jotai résulte souvent en moins de re-rendus. Pour un état interconnecté qui change moins fréquemment, l’approche de Zustand peut être plus efficace.
Intégration TypeScript
Les deux bibliothèques fournissent un excellent support TypeScript, mais avec des approches différentes.
Zustand avec TypeScript
interface BearState {
bears: number;
increase: (by: number) => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));
Jotai avec TypeScript
interface User {
id: string;
name: string;
}
const userAtom = atom<User | null>(null);
const nameAtom = atom(
(get) => get(userAtom)?.name || '',
(get, set, newName: string) => {
const user = get(userAtom);
if (user) {
set(userAtom, { ...user, name: newName });
}
}
);
Quand choisir Zustand
Zustand est souvent le meilleur choix quand :
-
Vous avez besoin d’un store centralisé : Pour les applications avec un état interconnecté qui doit être accessible et modifiable depuis de nombreux composants.
-
Vous migrez depuis Redux : L’API de Zustand est plus familière aux utilisateurs de Redux, facilitant la migration.
-
Vous avez besoin d’un accès à l’état hors React : Zustand permet d’accéder et de modifier l’état en dehors des composants React.
-
La collaboration en équipe est une priorité : L’approche du store centralisé peut être plus facile à maintenir dans de grandes équipes.
-
Vous préférez les mises à jour d’état explicites : L’approche de Zustand rend les changements d’état plus traçables.
Quand choisir Jotai
Jotai brille quand :
-
Vous avez besoin d’une réactivité fine : Pour les interfaces utilisateur avec de nombreux éléments d’état indépendants qui changent fréquemment.
-
Vous construisez des formulaires complexes : L’approche atomique de Jotai fonctionne bien pour les champs de formulaire qui doivent être validés indépendamment.
-
Vous voulez une API similaire à useState : Si vous préférez une API qui ressemble étroitement aux hooks intégrés de React.
-
Vous travaillez avec des données qui changent rapidement : Pour les applications temps réel où minimiser les re-rendus est critique.
-
Vous avez besoin d’état dérivé : Jotai facilite la création de valeurs calculées basées sur d’autres états.
Modèles d’implémentation du monde réel
Regardons quelques modèles courants implémentés dans les deux bibliothèques.
État d’authentification
Avec Zustand :
const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
isLoading: false,
login: async (credentials) => {
set({ isLoading: true });
try {
const user = await loginApi(credentials);
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: async () => {
await logoutApi();
set({ user: null, isAuthenticated: false });
}
}));
Avec Jotai :
const userAtom = atom(null);
const isAuthenticatedAtom = atom((get) => !!get(userAtom));
const isLoadingAtom = atom(false);
const loginAtom = atom(
null,
async (get, set, credentials) => {
set(isLoadingAtom, true);
try {
const user = await loginApi(credentials);
set(userAtom, user);
set(isLoadingAtom, false);
} catch (error) {
set(isLoadingAtom, false);
throw error;
}
}
);
const logoutAtom = atom(
null,
async (get, set) => {
await logoutApi();
set(userAtom, null);
}
);
Gestion d’état de formulaire
Avec Zustand :
const useFormStore = create((set) => ({
values: { name: '', email: '', message: '' },
errors: {},
setField: (field, value) => set(state => ({
values: { ...state.values, [field]: value }
})),
validate: () => {
// Logique de validation
const errors = {};
set({ errors });
return Object.keys(errors).length === 0;
},
submit: () => {
// Logique de soumission
}
}));
Avec Jotai :
const formAtom = atom({ name: '', email: '', message: '' });
const nameAtom = atom(
(get) => get(formAtom).name,
(get, set, name) => set(formAtom, { ...get(formAtom), name })
);
const emailAtom = atom(
(get) => get(formAtom).email,
(get, set, email) => set(formAtom, { ...get(formAtom), email })
);
const messageAtom = atom(
(get) => get(formAtom).message,
(get, set, message) => set(formAtom, { ...get(formAtom), message })
);
const errorsAtom = atom({});
const validateAtom = atom(
null,
(get, set) => {
// Logique de validation
const errors = {};
set(errorsAtom, errors);
return Object.keys(errors).length === 0;
}
);
Stratégies de migration
De Redux vers Zustand
// Redux
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
// Zustand
const useCounterStore = create((set) => ({
value: 0,
increment: () => set(state => ({ value: state.value + 1 })),
decrement: () => set(state => ({ value: state.value - 1 }))
}));
De l’API Context vers Jotai
// API Context
const CounterContext = createContext();
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
);
}
// Jotai
const countAtom = atom(0);
function App() {
return <>{/* Aucun provider nécessaire */}</>;
}
Pièges courants et bonnes pratiques
Pièges de Zustand
- Fragmentation du store : Créer trop de stores peut conduire à une confusion dans la gestion d’état.
- Mémorisation des sélecteurs : Oublier de mémoriser les sélecteurs peut causer des re-rendus inutiles.
- Surutilisation des middleware : Ajouter trop de middleware peut impacter les performances.
Pièges de Jotai
- Prolifération d’atomes : Créer trop d’atomes sans organisation peut rendre le code difficile à suivre.
- Dépendances circulaires : Créer des atomes qui dépendent les uns des autres de manière circulaire.
- Duplication d’atomes : Créer accidentellement plusieurs instances du même atome.
Bonnes pratiques pour les deux
- Organiser l’état lié : Grouper l’état et les actions liés ensemble.
- Utiliser TypeScript : Les deux bibliothèques bénéficient de la sécurité de type de TypeScript.
- Documenter votre structure d’état : Clarifier comment votre état est organisé.
- Tester votre logique d’état : Écrire des tests unitaires pour votre code de gestion d’état.
Conclusion
Choisir entre Zustand et Jotai dépend de vos exigences spécifiques de projet. Zustand offre une approche centralisée qui fonctionne bien pour un état complexe et interconnecté dans des applications plus importantes. Jotai fournit un modèle atomique qui excelle dans la réactivité fine avec un minimum de re-rendus. Les deux bibliothèques offrent des solutions légères et performantes qui améliorent significativement la complexité de Redux tout en maintenant la compatibilité TypeScript.
Considérez la familiarité de votre équipe avec différents modèles de gestion d’état, les besoins de performance de votre application, et votre structure d’état lors de votre décision. Rappelez-vous que vous pouvez même utiliser les deux bibliothèques dans la même application, en tirant parti de chacune pour ce qu’elle fait de mieux.
FAQ
Oui, de nombreux développeurs utilisent Zustand pour l'état global de l'application et Jotai pour l'état spécifique aux composants qui doit être partagé dans l'arbre des composants.
Oui, les deux peuvent évoluer vers de grandes applications lorsqu'elles sont utilisées correctement. Zustand pourrait être plus facile à maintenir dans des environnements de grande équipe en raison de son approche centralisée.
Les deux sont significativement plus légères et plus simples que Redux. Zustand est plus proche de Redux en philosophie mais avec beaucoup moins de code répétitif. Jotai adopte une approche complètement différente axée sur l'état atomique.
Zustand ne nécessite pas de provider par défaut. Jotai peut être utilisé sans provider pour les atomes globaux mais offre un provider pour l'état à portée limitée.
Oui, les deux bibliothèques fonctionnent bien avec Next.js et d'autres frameworks React. Elles fournissent des utilitaires spécifiques pour le support du rendu côté serveur.