Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    React

    Gestion d'état React en 2024: quelle solution choisir ?

    Équipe IT INNOVE
    10 min de lecture

    Zustand, Redux Toolkit, Jotai: comparatif complet pour optimiser votre state management.

    Gestion d'état React en 2024: quelle solution choisir ? - React | IT INNOVE
    Gestion d'état React en 2024: quelle solution choisir ? - React

    Gestion d'état React en 2024: quelle solution choisir ?

    Le state management React a évolué. Entre Context, Redux, Zustand, et les nouvelles approches atomiques, voici comment choisir la solution optimale selon votre projet.

    État des lieux 2024

    **Tendances observées sur nos projets
    **
    • 60% utilisent Zustand (nouvelles apps)
    • 25% restent sur Redux Toolkit (legacy + grosses équipes)
    • 10% adoptent Jotai/Recoil (state complexe)
    • 5% Context API uniquement (apps simples)

    1. Context API: quand suffit-il ?

    Use case optimal

    • Applications < 10 composants
    • État simple (user, theme, language)
    • Pas de logique métier complexe
    tsx
    // ✅ Bon usage: thème global simple
    interface ThemeContext {
      theme: 'light' | 'dark';
      toggleTheme: () => void;
    }
    
    const ThemeContext = createContext<ThemeContext | undefined>(undefined);
    
    export function ThemeProvider({ children }: { children: React.ReactNode }) {
      const [theme, setTheme] = useState<'light' | 'dark'>('light');
      
      const toggleTheme = useCallback(() => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
      }, []);
      
      // Mémorisation cruciale pour éviter les re-renders
      const value = useMemo(() => ({
        theme,
        toggleTheme
      }), [theme, toggleTheme]);
    
      return (
        <ThemeContext.Provider value={value}>
          {children}
        </ThemeContext.Provider>
      );
    }
    
    export function useTheme() {
      const context = useContext(ThemeContext);
      if (!context) {
        throw new Error('useTheme must be used within ThemeProvider');
      }
      return context;
    }

    ❌ Anti-pattern: Context pour tout

    tsx
    // Performance killer - provoque des re-renders en cascade
    const AppContext = createContext({
      user: null,
      cart: [],
      products: [],
      filters: {},
      notifications: [],
      // ... 20 autres propriétés
    });

    2. Zustand: le nouveau standard

    Pourquoi Zustand domine 2024

    • Simplicité: 2KB, API minimaliste
    • Performance: Pas de providers, sélecteurs granulaires
    • DevX: TypeScript natif, middleware riche
    • Flexible: Fonctionne avec ou sans React

    Setup basique

    tsx
    import { create } from 'zustand';
    import { devtools, persist } from 'zustand/middleware';
    
    interface CartState {
      items: CartItem[];
      total: number;
      addItem: (item: Product) => void;
      removeItem: (id: string) => void;
      clearCart: () => void;
    }
    
    export const useCartStore = create<CartState>()(
      devtools(
        persist(
          (set, get) => ({
            items: [],
            total: 0,
            
            addItem: (product) => set((state) => {
              const existingItem = state.items.find(item => item.id === product.id);
              
              if (existingItem) {
                return {
                  items: state.items.map(item =>
                    item.id === product.id
                      ? { ...item, quantity: item.quantity + 1 }
                      : item
                  ),
                  total: state.total + product.price
                };
              }
              
              return {
                items: [...state.items, { ...product, quantity: 1 }],
                total: state.total + product.price
              };
            }),
            
            removeItem: (id) => set((state) => {
              const item = state.items.find(item => item.id === id);
              if (!item) return state;
              
              return {
                items: state.items.filter(item => item.id !== id),
                total: state.total - (item.price * item.quantity)
              };
            }),
            
            clearCart: () => set({ items: [], total: 0 })
          }),
          {
            name: 'cart-storage',
            partialize: (state) => ({ 
              items: state.items,
              total: state.total
            })
          }
        ),
        { name: 'cart-store' }
      )
    );

    Usage avec sélecteurs optimisés

    tsx
    function CartSummary() {
      // ✅ Sélecteur granulaire - ne re-render que si total change
      const total = useCartStore(state => state.total);
      const itemCount = useCartStore(state => state.items.length);
      
      return (
        <div className="cart-summary">
          <span>{itemCount} articles</span>
          <span>{total}€</span>
        </div>
      );
    }
    
    function CartItems() {
      // ✅ Sélecteur avec shallow pour arrays/objets
      const { items, removeItem } = useCartStore(
        state => ({ 
          items: state.items, 
          removeItem: state.removeItem 
        }),
        shallow
      );
      
      return (
        <div>
          {items.map(item => (
            <CartItem 
              key={item.id}
              item={item}
              onRemove={() => removeItem(item.id)}
            />
          ))}
        </div>
      );
    }

    3. Redux Toolkit: pour les gros projets

    Quand choisir RTK ?

    • Équipes 10+ développeurs
    • Applications complexes (finance, ERP)
    • Besoins de debugging avancé (time-travel)
    • Pattern prévisible requis

    Setup moderne avec RTK Query

    tsx
    // store/api.ts
    import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
    
    export const api = createApi({
      reducerPath: 'api',
      baseQuery: fetchBaseQuery({
        baseUrl: '/api/',
        prepareHeaders: (headers, { getState }) => {
          const token = (getState() as RootState).auth.token;
          if (token) {
            headers.set('authorization', `Bearer ${token}`);
          }
          return headers;
        }
      }),
      tagTypes: ['User', 'Post'],
      endpoints: (builder) => ({
        getUsers: builder.query<User[], void>({
          query: () => 'users',
          providesTags: ['User']
        }),
        createPost: builder.mutation<Post, Partial<Post>>({
          query: (newPost) => ({
            url: 'posts',
            method: 'POST',
            body: newPost
          }),
          invalidatesTags: ['Post']
        })
      })
    });
    
    export const { useGetUsersQuery, useCreatePostMutation } = api;

    Slice avec Immer intégré

    tsx
    // store/authSlice.ts
    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    
    interface AuthState {
      user: User | null;
      token: string | null;
      isLoading: boolean;
    }
    
    const initialState: AuthState = {
      user: null,
      token: null,
      isLoading: false
    };
    
    export const authSlice = createSlice({
      name: 'auth',
      initialState,
      reducers: {
        loginStart: (state) => {
          state.isLoading = true; // Immer permet la mutation directe
        },
        loginSuccess: (state, action: PayloadAction<{ user: User; token: string }>) => {
          state.user = action.payload.user;
          state.token = action.payload.token;
          state.isLoading = false;
        },
        loginFailure: (state) => {
          state.isLoading = false;
          state.user = null;
          state.token = null;
        },
        logout: (state) => {
          state.user = null;
          state.token = null;
        }
      }
    });
    
    export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;

    4. Jotai: state atomique granulaire

    Concept: bottom-up state

    • Atoms indépendants et composables
    • Pas de boilerplate pour ajouter du state
    • Re-renders ultra optimisés
    tsx
    import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
    
    // Atoms primitifs
    const countAtom = atom(0);
    const nameAtom = atom('');
    
    // Atom dérivé (computed)
    const doubleCountAtom = atom(get => get(countAtom) * 2);
    
    // Atom avec actions
    const todoListAtom = atom<Todo[]>([]);
    const todoActionsAtom = atom(
      null, // pas de valeur en lecture
      (get, set, action: { type: 'add' | 'remove'; payload: any }) => {
        const todos = get(todoListAtom);
        
        switch (action.type) {
          case 'add':
            set(todoListAtom, [...todos, action.payload]);
            break;
          case 'remove':
            set(todoListAtom, todos.filter(t => t.id !== action.payload));
            break;
        }
      }
    );
    
    // Usage dans les composants
    function Counter() {
      const [count, setCount] = useAtom(countAtom);
      const doubleCount = useAtomValue(doubleCountAtom);
      
      return (
        <div>
          <p>Count: {count}</p>
          <p>Double: {doubleCount}</p>
          <button onClick={() => setCount(c => c + 1)}>+</button>
        </div>
      );
    }
    
    function TodoList() {
      const todos = useAtomValue(todoListAtom);
      const dispatch = useSetAtom(todoActionsAtom);
      
      return (
        <div>
          {todos.map(todo => (
            <div key={todo.id}>
              {todo.text}
              <button onClick={() => dispatch({ type: 'remove', payload: todo.id })}>
                Delete
              </button>
            </div>
          ))}
        </div>
      );
    }

    Atoms persistants et asynchrones

    tsx
    // Persistence
    import { atomWithStorage } from 'jotai/utils';
    const themeAtom = atomWithStorage('theme', 'light');
    
    // Async atoms
    const userAtom = atom(async () => {
      const response = await fetch('/api/user');
      return response.json();
    });
    
    // Atom dérivé async
    const userPostsAtom = atom(async (get) => {
      const user = await get(userAtom);
      const response = await fetch(`/api/users/${user.id}/posts`);
      return response.json();
    });
    
    function UserPosts() {
      const posts = useAtomValue(userPostsAtom);
      
      return (
        <Suspense fallback={<PostsSkeleton />}>
          <PostsList posts={posts} />
        </Suspense>
      );
    }

    Guide de décision

    Matrice projet → solution

    | Critères | Context | Zustand | RTK | Jotai |
    |----------|---------|---------|-----|-------|
    | Taille équipe | 1-3 | 2-8 | 5+ | 2-6 |
    | Complexité state | Simple | Moyen | Élevé | Variable |
    | Performance critique | ❌ | ✅ | ⚠️ | ✅ |
    | DevTools | Basiques | Bonnes | Excellentes | Bonnes |
    | Learning curve | Faible | Faible | Moyenne | Moyenne |
    | TypeScript | ⚠️ | ✅ | ✅ | ✅ |
    | Bundle size | 0KB | 2KB | 15KB | 5KB |

    Décision par use case

    tsx
    // ✅ Context: Thème, i18n, user session
    const ThemeContext = createContext();
    
    // ✅ Zustand: E-commerce, dashboards, apps moyennes
    const useStore = create(() => ({ ... }));
    
    // ✅ RTK: Finance, admin, apps d'entreprise
    const store = configureStore({ ... });
    
    // ✅ Jotai: Apps expérimentales, state très granulaire
    const dataAtom = atom(initialData);

    Migration patterns

    Context → Zustand

    tsx
    // Avant: Context API
    const CartContext = createContext();
    
    // Après: Zustand (même API)
    const useCart = create((set) => ({
      items: [],
      addItem: (item) => set(state => ({
        items: [...state.items, item]
      }))
    }));
    
    // Migration composant par composant
    function CartButton() {
      // const { addItem } = useContext(CartContext); // Avant
      const addItem = useCart(state => state.addItem); // Après
      
      return <button onClick={() => addItem(product)}>Add</button>;
    }

    Redux → RTK

    tsx
    // Avant: Redux classique
    const ADD_TODO = 'ADD_TODO';
    const addTodo = (text) => ({ type: ADD_TODO, payload: text });
    
    // Après: RTK Slice
    const todoSlice = createSlice({
      name: 'todos',
      initialState: [],
      reducers: {
        addTodo: (state, action) => {
          state.push({ id: Date.now(), text: action.payload });
        }
      }
    });

    Performance monitoring

    Profiler les re-renders

    tsx
    import { Profiler } from 'react';
    
    function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
      // Logger les re-renders coûteux
      if (actualDuration > 16) {
        console.warn(`Slow render in ${id}: ${actualDuration}ms`);
      }
    }
    
    <Profiler id="CartSection" onRender={onRender}>
      <CartSummary />
      <CartItems />
    </Profiler>

    DevTools par solution

    tsx
    // Zustand DevTools
    const useStore = create(
      devtools((set) => ({ ... }), { name: 'my-store' })
    );
    
    // Redux DevTools (intégré RTK)
    const store = configureStore({
      reducer: rootReducer,
      devTools: process.env.NODE_ENV !== 'production'
    });
    
    // Jotai DevTools
    import { DevTools } from 'jotai-devtools';
    
    function App() {
      return (
        <>
          <MyApp />
          {process.env.NODE_ENV === 'development' && <DevTools />}
        </>
      );
    }

    Conclusion & recommandations 2024

    Pour 90% des projets: **Zustand**

    • Simplicité + performance optimale
    • TypeScript excellent
    • Écosystème mature

    Pour les gros projets: **RTK + RTK Query**

    • Debugging avancé indispensable
    • Équipes nombreuses
    • Patterns stricts requis

    Pour l'expérimentation: **Jotai**

    • State très granulaire
    • Composabilité maximale
    • Apps avec besoins spécifiques

    Le choix dépend plus de votre équipe et contraintes que de la "meilleure" solution technique. Commencez simple, évoluez selon les besoins.

    Questions fréquentes