Gestion d'état avec Redux Toolkit
Dans cet article, je vais vous montrer comment j'ai structuré la gestion d'état de mon application avec Redux Toolkit, en partant d'un exemple concret : une plateforme d'épreuves et de quiz.
Le problème : Le "Props Drilling"
Quand votre application grandit, passer des props de parent en enfant devient un véritable cauchemar. Voici un exemple typique :
// ❌ Avant : Props drilling
function App() {
const [user, setUser] = useState(null);
const [epreuves, setEpreuves] = useState([]);
const [favoris, setFavoris] = useState([]);
return (
<Dashboard
user={user}
setUser={setUser}
epreuves={epreuves}
setEpreuves={setEpreuves}
favoris={favoris}
setFavoris={setFavoris}
/>
);
}
function Dashboard({ user, setUser, epreuves, setEpreuves, favoris, setFavoris }) {
return <Sidebar user={user} epreuves={epreuves} favoris={favoris} />;
}
function Sidebar({ user, epreuves, favoris }) {
return <UserProfile user={user} />;
}
function UserProfile({ user }) {
// On a passé user à travers 3 composants juste pour l'afficher ici
return <div>Bonjour {user?.name}</div>;
}
Problèmes identifiés :
- Code redondant et difficile à maintenir
- Ré-rendus inutiles des composants intermédiaires
- Risque d'erreurs en modifiant la structure
- Difficulté à tracer d'où viennent les données
La solution : Redux Toolkit
Redux Toolkit est la solution officielle recommandée par l'équipe Redux. Il apporte une structure claire, prévisible et facile à maintenir.
1. Installation
npm install @reduxjs/toolkit react-redux
2. Structure du store
Voici la structure que j'utilise dans mon projet actuel :
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import authSlice from './slices/authSlice';
import tokenReducer from './slices/tokenSlice';
import profileReducer from './slices/profileSlice';
import epreuvesReducer from './slices/epreuvesSlice';
import favorisReducer from './slices/favoriSlice';
import quizReducer from './slices/quizSlice';
import premiumReducer from './slices/premiumSlice';
import purchasesReducer from './slices/purchasesSlice';
import checkoutReducer from './slices/checkoutSlice';
export const store = configureStore({
reducer: {
auth: authSlice,
token: tokenReducer,
profile: profileReducer,
epreuves: epreuvesReducer,
favoris: favorisReducer,
quiz: quizReducer,
premium: premiumReducer,
purchases: purchasesReducer,
checkout: checkoutReducer,
},
devTools: process.env.NODE_ENV !== 'production',
});
3. Création d'un slice (exemple avec auth)
// store/slices/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const loginUser = createAsyncThunk(
'auth/login',
async ({ email, password }, { rejectWithValue }) => {
try {
const response = await axios.post('/api/login', { email, password });
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
const initialState = {
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem('token');
},
setUser: (state, action) => {
state.user = action.payload;
state.isAuthenticated = true;
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
localStorage.setItem('token', action.payload.token);
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload?.message || 'Erreur de connexion';
});
},
});
export const { logout, setUser, clearError } = authSlice.actions;
export default authSlice.reducer;
4. Exemple concret : Gestion des épreuves
Voici un extrait de mon epreuvesSlice.js qui montre comment gérer un état complexe :
// store/slices/epreuvesSlice.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
const ITEMS_PER_PAGE = 9;
export const getEpreuves = createAsyncThunk(
'epreuves/getEpreuves',
async (_, { rejectWithValue }) => {
try {
const token = localStorage.getItem('token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await axios.get(`${BASE_URL}/api/epreuves`, { headers });
return {
epreuves: response.data.epreuves || [],
matieres: response.data.matieres || [],
annees: response.data.annees || [],
series: response.data.series || [],
isAuthenticated: response.data.isAuthenticated || false,
isPremium: response.data.isPremium || false,
};
} catch (error) {
return rejectWithValue(error.response?.data || error.message);
}
},
{
condition: (_, { getState }) => {
const { loading, unFilteredEpreuves } = getState().epreuves;
if (loading || unFilteredEpreuves.length > 0) return false;
},
}
);
const computeFiltered = (unFiltered, filters, searchQuery, activePill) => {
const { annee, matiere, serie } = filters;
return unFiltered.filter((epreuve) => {
const anneeMatch = !annee || epreuve.annee === annee;
const matiereMatch = !matiere || epreuve.matiere === matiere;
const serieMatch = !serie || epreuve.series?.some(s => s.serie === serie);
const pillMatch = !activePill || activePill === 'all' || epreuve.matiere === activePill;
const searchMatch = !searchQuery ||
epreuve.matiere.toLowerCase().includes(searchQuery.toLowerCase()) ||
epreuve.annee.includes(searchQuery);
return anneeMatch && matiereMatch && serieMatch && pillMatch && searchMatch;
});
};
export const epreuveSlice = createSlice({
name: 'epreuves',
initialState: {
loading: false,
error: null,
epreuves: [],
unFilteredEpreuves: [],
matieres: [],
annees: [],
series: [],
filters: { annee: '', serie: '', matiere: '' },
searchQuery: '',
activePill: 'all',
currentPage: 1,
totalPages: 0,
isAuthenticated: false,
isPremium: false,
pdfCache: {},
},
reducers: {
setSearchQuery: (state, { payload }) => {
state.searchQuery = payload;
const filtered = computeFiltered(
state.unFilteredEpreuves,
state.filters,
payload,
state.activePill
);
state.epreuves = filtered;
state.totalPages = Math.ceil(filtered.length / ITEMS_PER_PAGE);
state.currentPage = 1;
},
applyFilter: (state, { payload }) => {
state.filters = { ...state.filters, ...payload };
const filtered = computeFiltered(
state.unFilteredEpreuves,
state.filters,
state.searchQuery,
state.activePill
);
state.epreuves = filtered;
state.totalPages = Math.ceil(filtered.length / ITEMS_PER_PAGE);
state.currentPage = 1;
},
resetFilters: (state) => {
state.filters = { annee: '', serie: '', matiere: '' };
state.searchQuery = '';
state.activePill = 'all';
state.epreuves = [...state.unFilteredEpreuves];
state.totalPages = Math.ceil(state.unFilteredEpreuves.length / ITEMS_PER_PAGE);
state.currentPage = 1;
},
},
extraReducers: (builder) => {
builder
.addCase(getEpreuves.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getEpreuves.fulfilled, (state, { payload }) => {
state.unFilteredEpreuves = payload.epreuves;
state.epreuves = payload.epreuves;
state.matieres = payload.matieres;
state.annees = payload.annees;
state.series = payload.series;
state.isAuthenticated = payload.isAuthenticated;
state.isPremium = payload.isPremium;
state.totalPages = Math.ceil(payload.epreuves.length / ITEMS_PER_PAGE);
state.loading = false;
})
.addCase(getEpreuves.rejected, (state, { payload }) => {
state.loading = false;
state.error = payload?.message || 'Erreur de chargement';
});
},
});
export const { setSearchQuery, applyFilter, resetFilters } = epreuveSlice.actions;
export default epreuveSlice.reducer;
5. Utilisation dans les composants React
Voici comment utiliser Redux Toolkit dans vos composants :
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getEpreuves, setSearchQuery, applyFilter } from '../store/slices/epreuvesSlice';
import { loginUser } from '../store/slices/authSlice';
function Dashboard() {
const dispatch = useDispatch();
const { epreuves, loading, matieres, isAuthenticated } = useSelector(
(state) => state.epreuves
);
const { user } = useSelector((state) => state.auth);
useEffect(() => {
dispatch(getEpreuves());
}, [dispatch]);
const handleSearch = (query) => {
dispatch(setSearchQuery(query));
};
const handleFilter = (filter) => {
dispatch(applyFilter(filter));
};
if (loading) return <div>Chargement...</div>
return (
<div>
<h1>Bonjour {user?.name || 'Visiteur'}</h1>
<input
type="text"
placeholder="Rechercher..."
onChange={(e) => handleSearch(e.target.value)}
/>
<select onChange={(e) => handleFilter({ matiere: e.target.value })}>
<option value="">Toutes les matières</option>
{matieres.map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
<div className="grid">
{epreuves.map(epreuve => (
<EpreuveCard key={epreuve.id} epreuve={epreuve} />
))}
</div>
</div>
);
}
Les avantages concrets
- ✅ Centralisation : Toute la logique métier est regroupée dans les slices
- ✅ Pas de props drilling : N'importe quel composant accède directement à l'état
- ✅ Debug facile : Redux DevTools permet de tracer chaque action
- ✅ Performance : Seuls les composants concernés sont re-rendus
- ✅ Code propre : Structure modulaire et facile à maintenir
Les bonnes pratiques
✅ À faire
- Utiliser createSlice pour chaque fonctionnalité
- Mémoiser les sélecteurs avec createSelector
- Utiliser createAsyncThunk pour les appels API
- Structurer par domaine (auth, profile, epreuves, etc.)
❌ À éviter
- Muter l'état directement dans les reducers
- Stocker l'état UI local dans Redux
- Créer des actions trop génériques
- Tout mettre dans un seul slice
Conclusion
Redux Toolkit a transformé ma façon de développer. L'architecture est claire, le code est maintenable, et je ne passe plus ma vie à gérer des props qui traversent 5 composants.
Si votre projet commence à grossir, adoptez Redux Toolkit. Vous ne regretterez pas !
Prochain article : Je vous montrerai comment gérer l'authentification JWT et les requêtes API asynchrones avec createAsyncThunk.