Redux Toolkit : de First Slice aux données asynchrones – un modèle mental opérationnel
Apprenez comment le store de Redux Toolkit, les slices, les réducteurs alimentés par Immer, les sélecteurs et createAsyncThunk s’assemblent, à l’aide d’un compteur, d’un panier d’achat et d’une liste de produits.
Sources: Redux Toolkit: A Complete Guide from Basics to Production
Au fur et à mesure que l’application React se développe, les états partagés tels que l’utilisateur, le panier, les préférences, les filtres et les données API se répandent sur de nombreuses pages, ce qui rend fragile le transfert de tous ces éléments via des props. Redux résout ce problème grâce à un stock centralisé, et Redux Toolkit (RTK) est la méthode recommandée pour utiliser Redux aujourd’hui. Ce guide vous guide, depuis une simple fonction de comptage jusqu’à un panier avec des totaux calculés et une liste alimentée par une API, afin que vous puissiez lire et écrire du code RTK avec confiance.
Tout d’abord, vérifiez si vous avez réellement besoin d’un stock : le simple usage de props n’est pas une raison suffisante pour installer Redux.
L’idée fondamentale derrière Redux
Redux conserve les états partagés dans un seul stock et ne les modifie qu’au moyen d’actions gérées par des reducers, ce qui rend chaque mise à jour prévisible. Sans lui, les données se transmettent étage par étage via des props.
Component
↓
Props
↓
Component
↓
Props
↓
Component
Avec un store, les composants lisent ce dont ils ont besoin et envoient des actions pour demander des modifications.
Redux Store
↑
│
┌─────────┴─────────┐
│ │
Component A Component B
│ │
└──── dispatch ─────┘
Ce que Redux Toolkit ajoute
RTK regroupe des outils pour les tâches courantes de Redux.
Redux Toolkit
│
├── configureStore()
├── createSlice()
├── createAsyncThunk()
├── createSelector()
└── RTK Query
Redux classique répartit une fonctionnalité sur de nombreux fichiers.
Action Types
Action Creators
Reducers
Constants
Store
Middleware
createSlice() vous permet d’exprimer la plupart de ces fonctionnalités en un seul endroit.
Comment les données circulent dans une application RTK
Les composants lisent les données avec useSelector() et envoient des actions avec useDispatch() ; les reducers génèrent un nouvel état.
React Application
│
↓
useSelector()
│
↓
Redux Store
↑
│
useDispatch()
↑
│
Actions
│
↓
Reducer
│
↓
New State
Le cycle essentiel :
UI
↓
dispatch(action)
↓
Reducer
↓
State update
↓
Subscribed components
↓
UI re-render
Installation et création du store
Installez deux paquets :
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit contient les API de Redux ; react-redux les relie à React.
useSelector()
useDispatch()
Provider
L’exemple d’application de compteur est organisé par fonctionnalité.
src/
│
├── app/
│ └── store.js
│
├── features/
│ └── counter/
│ └── counterSlice.js
│
├── App.jsx
└── main.jsx
configureStore() prend un dictionnaire de réducteurs de slice ; chaque clé devient une branche d’état de niveau supérieur.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
L’état résultant :
{
counter: {
value: 0
}
}
Slices : une fonctionnalité, un fichier
Un slice représente une partie logique de l’état, et les applications en possèdent généralement plusieurs.
authSlice
cartSlice
productSlice
userSlice
notificationSlice
Le slice du compteur définit un nom, un état initial ainsi qu’un réducteur par opération, puis exporte les créateurs d’actions et le réducteur générés.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
},
},
});
export const {
increment,
decrement,
incrementByAmount,
} = counterSlice.actions;
export default counterSlice.reducer;
Une seule appelation remplace trois parties écrites manuellement.
Action Types
+
Action Creators
+
Reducer
Ce que generateSlice produit
Prenons un slice minimal :
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value += 1;
},
},
});
RTK combine le nom du slice et la clé du réducteur pour former un type d’action
"counter/increment"
et crée un créateur d’action.
increment()
Ainsi, dispatch(increment()) est équivalent à l’envoi de :
dispatch({
type: "counter/increment",
});
Connexion du store à React
Dans main.jsx, Provider rend le store accessible à chaque composant qui se trouve en dessous de lui.
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Provider store={store}>
<App />
</Provider>
);
Lecture avec useSelector
useSelector() sélectionne précisément la partie de l’état dont un composant a besoin.
import { useSelector } from "react-redux";
function Counter() {
const count = useSelector(
state => state.counter.value
);
return <h1>{count}</h1>;
}
comment React-Redux décide de se rérendre.
Écriture avec useDispatch
useDispatch() renvoie la fonction dispatch du store.
import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";
function Counter() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}
Button Click
↓
dispatch(increment())
↓
Action
↓
Reducer
↓
State changes
↓
useSelector detects relevant change
↓
Component re-renders
Transfert de données avec payload
payload, qui provient du deuxième argument du réducteur :
incrementByAmount(state, action) {
state.value += action.payload;
}
dispatch(incrementByAmount(10));
{
type: "counter/incrementByAmount",
payload: 10
}
Chaque action possède un type et éventuellement un payload.
action
│
├── type
└── payload
Pourquoi la mutation de l’état est autorisée ici
C’est une question classique lors des entretiens : ce réducteur semble modifier l’état, ce que Redux interdit.
increment(state) {
state.value += 1;
}
RTK exécute les réducteurs via Immer, qui fournit un proxy provisoire, enregistre les modifications et génère un nouvel objet immuable.
Reducer
↓
Immer Proxy
↓
Track changes
↓
Create new immutable state
On obtient ainsi du code lisible sans compromettre l’immuabilité. Il faut soit modifier le draft, soit retourner une nouvelle valeur, jamais les deux en même temps, et il ne faut pas modifier l’état de cette manière en dehors des réducteurs RTK.
Une partie concernant le panier d’achat
Un panier de commerce électronique commence vide.
const initialState = {
items: [],
};
Cette partie permet d’ajouter, de supprimer et de vider des articles ; removeItem attribue un nouveau tableau, que Immer accepte également.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
items: [],
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addItem(state, action) {
state.items.push(action.payload);
},
removeItem(state, action) {
state.items = state.items.filter(
item => item.id !== action.payload
);
},
clearCart(state) {
state.items = [];
},
},
});
export const {
addItem,
removeItem,
clearCart,
} = cartSlice.actions;
export default cartSlice.reducer;
Ajout d’un article
Étant donné un objet produit :
const product = {
id: 101,
name: "Laptop",
price: 60000,
};
un gestionnaire de clic le envoie :
const dispatch = useDispatch();
const handleAddToCart = () => {
dispatch(addItem(product));
};
et l’ensemble du produit devient la charge utile.
{
type: "cart/addItem",
payload: {
id: 101,
name: "Laptop",
price: 60000
}
}
Le réducteur le met ensuite dans :
state.items
Suppression d’un élément
Filtres de suppression par id :
removeItem(state, action) {
state.items = state.items.filter(
item => item.id !== action.payload
);
}
Le composant ne transmet que l’id :
dispatch(removeItem(101));
donc à l’intérieur du réducteur :
action.payload === 101
Dériver des valeurs plutôt que de les stocker
Supposons que le panier contienne ces éléments :
[
{
price: 100,
quantity: 2,
},
{
price: 50,
quantity: 3,
}
]
Le total suit directement :
100 × 2 + 50 × 3
= 200 + 150
= 350
Il est tentant de stocker le total à côté des éléments :
{
items,
total
}
mais alors chaque réducteur doit maintenir les deux en synchronisation. Calculez-le plutôt :
const selectCartTotal = state =>
state.cart.items.reduce(
(total, item) =>
total + item.price * item.quantity,
0
);
et lisez-le :
const total = useSelector(selectCartTotal);
Stockez la source de vérité ; dérivez tout ce qui peut être calculé à partir d’elle.
Les sélecteurs en tant que point d’accès unique
Un sélecteur est une fonction qui lit l’état.
const selectCartItems = state =>
state.cart.items;
Les composants l’utilisent plutôt que de connaître la forme de l’état.
const items = useSelector(selectCartItems);
Sinon, ce chemin se répète partout :
state.cart.items
Avec un sélecteur, la restructuration ne concerne qu’une seule fonction.
Mémorisation avec createSelector
createSelector() cache un résultat dérivé jusqu’à ce que ses entrées changent.
import { createSelector } from "@reduxjs/toolkit";
const selectCartItems = state =>
state.cart.items;
export const selectCartTotal = createSelector(
[selectCartItems],
items =>
items.reduce(
(total, item) =>
total + item.price * item.quantity,
0
)
);
La logique est simple :
Input unchanged
↓
Return cached result
Input changed
↓
Recalculate
Cela aide pour les calculs coûteux ainsi que pour les sélecteurs qui retournent de nouveaux tableaux ou objets, ce qui entraînerait sinon un re-render à chaque mise à jour du store.
Chargement des données avec createAsyncThunk
Les applications appellent des API telles que :
GET /api/products
Une requête passe par l’état « idle », puis « loading » avant d’atteindre « success »
idle
↓
loading
↓
success
ou « error ».
idle
↓
loading
↓
error
createAsyncThunk() modélise cela : on lui donne un préfixe de type et une fonction asynchrone ; le fait de lancer une exception indique que la requête a échoué.
import {
createAsyncThunk,
createSlice,
} from "@reduxjs/toolkit";
export const fetchProducts = createAsyncThunk(
"products/fetchProducts",
async () => {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error("Failed to fetch products");
}
return response.json();
}
);
Cela génère trois actions liées au cycle de vie :
fetchProducts.pending
fetchProducts.fulfilled
fetchProducts.rejected
Gestion du cycle de vie dans une slice
La slice des produits suit les éléments, leur statut et les erreurs, en les mettant à jour dans extraReducers.
const productsSlice = createSlice({
name: "products",
initialState: {
items: [],
status: "idle",
error: null,
},
reducers: {},
extraReducers: builder => {
builder
.addCase(
fetchProducts.pending,
state => {
state.status = "loading";
}
)
.addCase(
fetchProducts.fulfilled,
(state, action) => {
state.status = "success";
state.items = action.payload;
}
)
.addCase(
fetchProducts.rejected,
(state, action) => {
state.status = "failed";
state.error =
action.error.message;
}
);
},
});
Reducers versus extraReducers
reducers définit les actions propres à la slice, avec des fonctions de création générées.
reducers: {
addItem() {}
}
extraReducers répond aux actions définies ailleurs, comme celles provenant de createAsyncThunk() :
pending
fulfilled
rejected
La slice ne fait que les écouter.
Déploiement du thunk depuis un composant
Démarrer la requête au chargement du composant :
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
Le mode strict exécute les effets deux fois en environnement de développement, donc on s’attend à deux requêtes. Ensuite sélectionner l’état
const {
items,
status,
error,
} = useSelector(
state => state.products
);
et afficher selon le statut :
if (status === "loading") {
return <p>Loading...</p>;
}
if (status === "failed") {
return <p>{error}</p>;
}
return (
<div>
{items.map(product => (
<div key={product.id}>
{product.name}
</div>
))}
</div>
);
Pour des chargements de données plus lourds, RTK Query peut remplacer ce schéma.
Moyen de transmission et configureStore
dispatch() et le réducteur.
dispatch(action)
↓
Middleware
↓
Reducer
↓
New State
Utilisations courantes :
- Logique asynchrone
- Journalisation
- Analyses
- Flux d’authentification
- Effets secondaires
- Interception des actions
configureStore() remplace la configuration manuelle en ajoutant un moyen de transmission par défaut (y compris des thunks) ainsi que Redux DevTools.
const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
products: productsReducer,
},
});
Points clés
- Une slice regroupe l’état, les réducteurs et les créateurs d’actions générés pour une fonctionnalité donnée.
- Immer rend les réducteurs de type mutation sûrs, mais uniquement à l’intérieur des réducteurs RTK.
- Conservez un état minimal ; déduisez le reste à l’aide de sélecteurs, en mémorisant les résultats lorsque cela est coûteux.
createAsyncThunk()combiné àextraReducerspermet de gérer les cas de chargement, de succès et d’échec.- Les paramètres par défaut de
configureStore()couvrent la plupart des applications.