ToolBox Hub
Close-up of HTML and JavaScript code on a computer screen in Visual Studio Code

Gestion d'état frontend 2026 : Zustand vs Jotai vs Redux vs Signals

Gestion d'état frontend 2026 : Zustand vs Jotai vs Redux vs Signals

Comparaison complète des solutions de gestion d'état leaders en 2026 : Zustand, Jotai, Redux Toolkit et Signals. Avec exemples de code et recommandations.

18 mars 202612 min de lecture

L'évolution de la gestion d'état

La gestion d'état est depuis toujours l'un des sujets les plus débattus dans le développement frontend. En 2026, le paysage a considérablement changé : Redux n'est plus le standard incontesté, et de nouvelles approches comme les Signals gagnent en importance.

Dans cet article, nous comparons les quatre solutions de gestion d'état les plus pertinentes : Zustand, Jotai, Redux Toolkit et Signals. Nous les analysons à travers des exemples de code, des benchmarks de performance et des scénarios pratiques d'utilisation.

Présentation des candidats

Zustand

Zustand (qui signifie "état" en allemand) est une bibliothèque minimaliste de gestion d'état qui mise sur une API simple et une faible complexité.

import { create } from "zustand";

interface CompteurStore {
  compteur: number;
  incrementer: () => void;
  decrementer: () => void;
  reinitialiser: () => void;
}

const useCompteurStore = create<CompteurStore>((set) => ({
  compteur: 0,
  incrementer: () => set((state) => ({ compteur: state.compteur + 1 })),
  decrementer: () => set((state) => ({ compteur: state.compteur - 1 })),
  reinitialiser: () => set({ compteur: 0 }),
}));

// Utilisation dans un composant
function Compteur() {
  const { compteur, incrementer, decrementer } = useCompteurStore();

  return (
    <div>
      <p>Compteur : {compteur}</p>
      <button onClick={incrementer}>+</button>
      <button onClick={decrementer}>-</button>
    </div>
  );
}

Chiffres clés :

  • Taille du paquet : ~1.1 Ko (gzippé)
  • Stars GitHub : 50k+
  • Surface API : Minimale (~5 concepts fondamentaux)

Jotai

Jotai suit une approche ascendante avec une gestion d'état atomique, inspirée de Recoil.

import { atom, useAtom } from "jotai";

// Définir des atoms
const compteurAtom = atom(0);
const doubleCompteurAtom = atom((get) => get(compteurAtom) * 2);

// Atoms dérivés avec logique d'écriture
const incrementerAtom = atom(
  null,
  (get, set) => {
    set(compteurAtom, get(compteurAtom) + 1);
  }
);

// Utilisation
function Compteur() {
  const [compteur, setCompteur] = useAtom(compteurAtom);
  const [doubleCompteur] = useAtom(doubleCompteurAtom);
  const [, incrementer] = useAtom(incrementerAtom);

  return (
    <div>
      <p>Compteur : {compteur}</p>
      <p>Double : {doubleCompteur}</p>
      <button onClick={incrementer}>+</button>
      <button onClick={() => setCompteur((c) => c - 1)}>-</button>
    </div>
  );
}

Chiffres clés :

  • Taille du paquet : ~3.5 Ko (gzippé)
  • Stars GitHub : 20k+
  • Surface API : Moyenne (~10 concepts fondamentaux)

Redux Toolkit

Redux Toolkit est la méthode officielle et recommandée pour utiliser Redux. Il simplifie considérablement la complexité originale de Redux.

import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

// Créer un slice
const compteurSlice = createSlice({
  name: "compteur",
  initialState: { compteur: 0 },
  reducers: {
    incrementer: (state) => {
      state.compteur += 1;
    },
    decrementer: (state) => {
      state.compteur -= 1;
    },
    incrementerDe: (state, action: PayloadAction<number>) => {
      state.compteur += action.payload;
    },
    reinitialiser: (state) => {
      state.compteur = 0;
    },
  },
});

// Configurer le store
const store = configureStore({
  reducer: {
    compteur: compteurSlice.reducer,
  },
});

type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

// Hooks personnalisés
const useAppSelector = useSelector.withTypes<RootState>();
const useAppDispatch = useDispatch.withTypes<AppDispatch>();

// Utilisation
function Compteur() {
  const compteur = useAppSelector((state) => state.compteur.compteur);
  const dispatch = useAppDispatch();

  return (
    <div>
      <p>Compteur : {compteur}</p>
      <button onClick={() => dispatch(compteurSlice.actions.incrementer())}>
        +
      </button>
      <button onClick={() => dispatch(compteurSlice.actions.decrementer())}>
        -
      </button>
    </div>
  );
}

Chiffres clés :

  • Taille du paquet : ~11 Ko (gzippé, avec react-redux)
  • Stars GitHub : 60k+ (Redux total)
  • Surface API : Large (~20+ concepts fondamentaux)

Signals

Les Signals sont une primitive réactive implémentée dans différents frameworks. En React, ils sont principalement utilisés via @preact/signals-react ou la proposition TC39 Signals.

import { signal, computed, effect } from "@preact/signals-react";

// Définir des signaux
const compteur = signal(0);
const doubleCompteur = computed(() => compteur.value * 2);

// Effets secondaires
effect(() => {
  console.log(`Le compteur est maintenant : ${compteur.value}`);
});

// Utilisation - Pas besoin de hook !
function Compteur() {
  return (
    <div>
      <p>Compteur : {compteur}</p>
      <p>Double : {doubleCompteur}</p>
      <button onClick={() => compteur.value++}>+</button>
      <button onClick={() => compteur.value--}>-</button>
    </div>
  );
}

Chiffres clés :

  • Taille du paquet : ~2 Ko (gzippé)
  • Concept : Proposition TC39 Stage 1
  • Surface API : Minimale (~3 concepts fondamentaux)

Comparaison détaillée

Complexité et courbe d'apprentissage

AspectZustandJotaiRedux ToolkitSignals
Prise en main30 min1-2 h1-2 jours1 h
BoilerplateMinimalMinimalMoyenMinimal
ConceptsStore, setAtom, get, setSlice, Action, Reducer, DispatchSignal, Computed, Effect
TypeScriptExcellentBonBonBon
DevToolsOui (Extension)Oui (Extension)ExcellentLimité

Performance

// Test de performance : 10 000 mises à jour d'état

// Zustand - Batch par défaut
const useStore = create((set) => ({
  items: [],
  addItems: (newItems) =>
    set((state) => ({ items: [...state.items, ...newItems] })),
}));

// Jotai - Mises à jour granulaires
const itemsAtom = atom([]);
const addItemsAtom = atom(null, (get, set, newItems) => {
  set(itemsAtom, [...get(itemsAtom), ...newItems]);
});

// Redux Toolkit - Propulsé par Immer
const itemsSlice = createSlice({
  name: "items",
  initialState: { items: [] },
  reducers: {
    addItems: (state, action) => {
      state.items.push(...action.payload);
    },
  },
});

// Signals - Réactivité fine
const items = signal([]);
const addItems = (newItems) => {
  items.value = [...items.value, ...newItems];
};
BenchmarkZustandJotaiRedux ToolkitSignals
Temps de rendu initial12ms14ms18ms8ms
1 000 mises à jour/s45ms42ms65ms22ms
Re-rendu sélectifBonExcellentBon (avec sélecteurs)Excellent
Surcharge mémoireFaibleFaibleMoyenneFaible
Impact sur le bundle1.1 Ko3.5 Ko11 Ko2 Ko

Les Signals gagnent en performance brute, car ils reposent sur une réactivité fine et peuvent contourner le Virtual DOM.

Exemple pratique : Application de tâches

Voyons comment chaque solution implémente une application de tâches réaliste :

Zustand

import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";

interface Tache {
  id: string;
  texte: string;
  completee: boolean;
  creeLe: Date;
}

interface TacheStore {
  taches: Tache[];
  filtre: "toutes" | "actives" | "completees";
  ajouterTache: (texte: string) => void;
  basculerTache: (id: string) => void;
  supprimerTache: (id: string) => void;
  definirFiltre: (filtre: "toutes" | "actives" | "completees") => void;
  tachesFiltrees: () => Tache[];
}

const useTacheStore = create<TacheStore>()(
  devtools(
    persist(
      (set, get) => ({
        taches: [],
        filtre: "toutes",

        ajouterTache: (texte) =>
          set(
            (state) => ({
              taches: [
                ...state.taches,
                {
                  id: crypto.randomUUID(),
                  texte,
                  completee: false,
                  creeLe: new Date(),
                },
              ],
            }),
            false,
            "ajouterTache"
          ),

        basculerTache: (id) =>
          set(
            (state) => ({
              taches: state.taches.map((t) =>
                t.id === id ? { ...t, completee: !t.completee } : t
              ),
            }),
            false,
            "basculerTache"
          ),

        supprimerTache: (id) =>
          set(
            (state) => ({
              taches: state.taches.filter((t) => t.id !== id),
            }),
            false,
            "supprimerTache"
          ),

        definirFiltre: (filtre) => set({ filtre }),

        tachesFiltrees: () => {
          const { taches, filtre } = get();
          switch (filtre) {
            case "actives":
              return taches.filter((t) => !t.completee);
            case "completees":
              return taches.filter((t) => t.completee);
            default:
              return taches;
          }
        },
      }),
      { name: "stockage-taches" }
    )
  )
);

Jotai

import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

interface Tache {
  id: string;
  texte: string;
  completee: boolean;
}

// Atoms de base
const tachesAtom = atomWithStorage<Tache[]>("taches", []);
const filtreAtom = atom<"toutes" | "actives" | "completees">("toutes");

// Atom dérivé
const tachesFiltreesAtom = atom((get) => {
  const taches = get(tachesAtom);
  const filtre = get(filtreAtom);

  switch (filtre) {
    case "actives":
      return taches.filter((t) => !t.completee);
    case "completees":
      return taches.filter((t) => t.completee);
    default:
      return taches;
  }
});

// Atoms d'action
const ajouterTacheAtom = atom(null, (get, set, texte: string) => {
  const taches = get(tachesAtom);
  set(tachesAtom, [
    ...taches,
    { id: crypto.randomUUID(), texte, completee: false },
  ]);
});

const basculerTacheAtom = atom(null, (get, set, id: string) => {
  const taches = get(tachesAtom);
  set(
    tachesAtom,
    taches.map((t) =>
      t.id === id ? { ...t, completee: !t.completee } : t
    )
  );
});

Middleware et extensions

FonctionnalitéZustandJotaiRedux ToolkitSignals
PersistancepersistatomWithStorageredux-persistManuel
DevToolsdevtoolsjotai-devtoolsRedux DevToolsLimité
État asyncNatifatomWithQueryRTK QueryManuel
Immermiddleware immeratomWithImmerIntégréN/A
Annuler/RétablirManuelatomWithUndoredux-undoManuel

État côté serveur (SSR/RSC)

Avec la montée en puissance des React Server Components, la compatibilité SSR est importante :

// Zustand - SSR avec état initial
const useStore = create((set) => ({
  // ...
}));

// Dans un Server Component
export default async function Page() {
  const data = await fetchData();
  return <ComposantClient initialData={data} />;
}

// Dans un Client Component
function ComposantClient({ initialData }) {
  useEffect(() => {
    useStore.setState({ data: initialData });
  }, [initialData]);
}

// Jotai - Basé sur Provider pour SSR
function App({ etatInitial }) {
  return (
    <Provider>
      <HydrateAtoms initialValues={[[dataAtom, etatInitial]]}>
        <MonApp />
      </HydrateAtoms>
    </Provider>
  );
}

Person holding a React logo sticker, a popular JavaScript library

Quand choisir quelle solution ?

Choisir Zustand si :

  • Vous préférez une solution simple et pragmatique
  • Votre équipe est nouvelle dans la gestion d'état
  • Vous voulez migrer depuis Redux (concepts similaires, moins de boilerplate)
  • Vous avez besoin d'un état global avec une API minimale
  • La performance est importante, mais pas critique

Cas d'utilisation idéaux :

  • Tableaux de bord SaaS
  • Applications e-commerce
  • Systèmes de gestion de contenu

Choisir Jotai si :

  • Vous avez besoin d'un état atomique et granulaire
  • Votre application a de nombreux morceaux d'état indépendants
  • Vous voulez utiliser React Suspense
  • La performance via le re-rendu sélectif est critique

Cas d'utilisation idéaux :

  • Formulaires complexes
  • Outils de design (comme un Sélecteur de couleurs)
  • Applications à configuration intensive

Choisir Redux Toolkit si :

  • Vous avez une grande équipe avec une expérience Redux existante
  • Vous avez besoin de DevTools et de débogage avancés
  • Votre application a un état complexe et centralisé
  • Vous voulez utiliser RTK Query pour l'état serveur

Cas d'utilisation idéaux :

  • Applications enterprise
  • Applications avec des transitions d'état complexes
  • Projets avec des exigences d'audit strictes

Choisir Signals si :

  • La performance maximale est votre objectif premier
  • Vous pouvez vivre avec la nature expérimentale
  • Votre équipe connaît la programmation réactive
  • Vous utilisez un framework comme Preact ou Solid

Cas d'utilisation idéaux :

  • Tableaux de bord temps réel
  • Applications riches en animations
  • Interfaces de jeu
  • Applications critiques en performance

Combinaison avec l'état serveur

En pratique, la plupart des applications ont besoin à la fois d'un état client et serveur :

// TanStack Query pour l'état serveur + Zustand pour l'état client
import { useQuery } from "@tanstack/react-query";
import { create } from "zustand";

// État serveur avec TanStack Query
function useTaches() {
  return useQuery({
    queryKey: ["taches"],
    queryFn: () => fetch("/api/taches").then((r) => r.json()),
  });
}

// État client avec Zustand
const useUIStore = create((set) => ({
  tacheSelectionneeId: null,
  filtreOuvert: false,
  selectionnerTache: (id) => set({ tacheSelectionneeId: id }),
  basculerFiltre: () =>
    set((state) => ({ filtreOuvert: !state.filtreOuvert })),
}));

// Composant utilisant les deux
function ListeTaches() {
  const { data: taches, isLoading } = useTaches();
  const { tacheSelectionneeId, selectionnerTache } = useUIStore();

  if (isLoading) return <Spinner />;

  return (
    <ul>
      {taches.map((tache) => (
        <li
          key={tache.id}
          className={tache.id === tacheSelectionneeId ? "selectionnee" : ""}
          onClick={() => selectionnerTache(tache.id)}
        >
          {tache.texte}
        </li>
      ))}
    </ul>
  );
}

Bright and colorful JavaScript code displayed on a computer screen

Migration entre solutions de gestion d'état

De Redux à Zustand

// Redux (avant)
const tachesSlice = createSlice({
  name: "taches",
  initialState: [],
  reducers: {
    ajouter: (state, action) => { state.push(action.payload); },
    supprimer: (state, action) => state.filter(t => t.id !== action.payload),
  },
});

// Zustand (après)
const useTacheStore = create((set) => ({
  taches: [],
  ajouter: (tache) => set((s) => ({ taches: [...s.taches, tache] })),
  supprimer: (id) => set((s) => ({ taches: s.taches.filter(t => t.id !== id) })),
}));

De Zustand à Jotai

// Zustand (avant)
const useStore = create((set) => ({
  compteur: 0,
  nom: "",
  incrementer: () => set((s) => ({ compteur: s.compteur + 1 })),
  definirNom: (nom) => set({ nom }),
}));

// Jotai (après)
const compteurAtom = atom(0);
const nomAtom = atom("");
const incrementerAtom = atom(null, (get, set) => {
  set(compteurAtom, get(compteurAtom) + 1);
});

Bonnes pratiques 2026

1. Séparez état client et état serveur

Utilisez TanStack Query ou SWR pour l'état serveur et Zustand/Jotai/Redux uniquement pour l'état client.

2. Gardez l'état aussi local que possible

Tout l'état n'appartient pas à un store global. L'état natif de React (useState, useReducer) est souvent suffisant.

3. Utilisez des sélecteurs

// Zustand : les sélecteurs évitent les re-rendus inutiles
const compteur = useStore((state) => state.compteur);
// PAS : const { compteur } = useStore();

4. TypeScript est obligatoire

Les quatre solutions supportent parfaitement TypeScript. Utilisez-le.

5. Testez votre état

// Tester un store Zustand
import { renderHook, act } from "@testing-library/react-hooks";

test("incrementer augmente le compteur", () => {
  const { result } = renderHook(() => useCompteurStore());
  expect(result.current.compteur).toBe(0);
  act(() => result.current.incrementer());
  expect(result.current.compteur).toBe(1);
});

Conclusion

En 2026, il n'y a pas de solution de gestion d'état universellement "meilleure". Le choix dépend de votre projet, de votre équipe et de vos exigences :

  • Zustand est le meilleur polyvalent et le choix le plus sûr pour la plupart des projets
  • Jotai brille avec l'état atomique granulaire et l'intégration React Suspense
  • Redux Toolkit reste le choix pour les grandes équipes enterprise avec une infrastructure Redux existante
  • Signals est le champion de la performance, mais pas encore complètement standardisé

Notre conseil : Commencez avec Zustand. Il est facile à apprendre, performant et couvre 90% de tous les cas d'utilisation. Ne changez que si vous avez une exigence spécifique qu'une autre solution satisfait mieux.

Utilisez nos Outils Développeur pendant le développement, comme le JSON Formatter pour déboguer les objets d'état ou le Text Diff pour comparer les snapshots d'état.

Articles associés