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
// ✅ 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
// 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
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
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
// 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é
// 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
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
// 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
// ✅ 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
// 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
// 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
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
// 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.

