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.