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.