Gestione dello stato con Redux Toolkit

In questo articolo vi mostrerò come ho strutturato la gestione dello stato della mia applicazione con Redux Toolkit, partendo da un esempio concreto: una piattaforma di prove e quiz.
Il problema: il "Props Drilling"
Quando la vostra applicazione cresce, passare le props da genitore a figlio diventa un vero incubo. Ecco un esempio tipico:
// ❌ 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>;
}
Problemi identificati:
  • Codice ridondante e difficile da mantenere
  • Ri-rendering inutili dei componenti intermedi
  • Rischio di errori nel modificare la struttura
  • Difficoltà nel tracciare da dove provengono i dati
La soluzione: Redux Toolkit
Redux Toolkit è la soluzione ufficiale raccomandata dal team di Redux. Offre una struttura chiara, prevedibile e facile da mantenere.
1. Installazione
npm install @reduxjs/toolkit react-redux
2. Struttura dello store
Ecco la struttura che utilizzo nel mio progetto attuale:
// 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. Creazione di uno slice (esempio con 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. Esempio concreto: gestione delle prove
Ecco un estratto del mio epreuvesSlice.js che mostra come gestire uno stato complesso:
// 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. Utilizzo nei componenti React
Ecco come utilizzare Redux Toolkit nei vostri componenti:
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>
    );
}
I vantaggi concreti
  • ✅ Centralizzazione: tutta la logica di business è raggruppata negli slice
  • ✅ Nessun props drilling: qualsiasi componente accede direttamente allo stato
  • ✅ Debug facile: Redux DevTools permette di tracciare ogni azione
  • ✅ Prestazioni: vengono ri-renderizzati solo i componenti coinvolti
  • ✅ Codice pulito: struttura modulare e facile da mantenere
Le buone pratiche
✅ Da fare
  • Utilizzare createSlice per ogni funzionalità
  • Memorizzare i selettori con createSelector
  • Utilizzare createAsyncThunk per le chiamate API
  • Strutturare per dominio (auth, profile, epreuves, ecc.)
❌ Da evitare
  • Mutare lo stato direttamente nei reducer
  • Memorizzare lo stato UI locale in Redux
  • Creare azioni troppo generiche
  • Mettere tutto in un unico slice
Conclusione
Redux Toolkit ha trasformato il mio modo di sviluppare. L'architettura è chiara, il codice è mantenibile e non passo più la vita a gestire props che attraversano 5 componenti.
BSe il vostro progetto inizia a crescere, adottate Redux Toolkit. Non ve ne pentirete!
Prossimo articolo: vi mostrerò come gestire l'autenticazione JWT e le richieste API asincrone con createAsyncThunk.