# Chapitre 3 : Redux et Redux Toolkit ## Pourquoi Redux ? Context est bien pour des données simples (thème, utilisateur). Mais pour des applications complexes : - État partagé entre beaucoup de composants - Logique de mise à jour complexe - Besoin d'un historique des actions (debug) - Performance avec de nombreuses mises à jour Redux offre une solution structurée et prévisible. ## Les concepts fondamentaux ### 1. Store Le **store** est l'unique source de vérité. Tout l'état de l'application est dans un seul objet. ```javascript { user: { id: 1, name: 'Jean' }, products: [...], cart: { items: [], total: 0 }, ui: { isLoading: false, error: null } } ``` ### 2. Actions Les **actions** décrivent ce qui s'est passé. Ce sont des objets avec un `type` : ```javascript { type: 'cart/addItem', payload: { id: 1, name: 'Produit', price: 29.99 } } { type: 'user/login', payload: { id: 1, name: 'Jean' } } { type: 'ui/setLoading', payload: true } ``` ### 3. Reducers Les **reducers** spécifient comment l'état change en réponse à une action : ```javascript function cartReducer(state, action) { switch (action.type) { case 'cart/addItem': return { ...state, items: [...state.items, action.payload] }; case 'cart/removeItem': return { ...state, items: state.items.filter(item => item.id !== action.payload) }; default: return state; } } ``` ### Le flux Redux ``` Action → Reducer → Nouveau State → UI mise à jour ``` 1. Un événement déclenche une action 2. Le reducer calcule le nouvel état 3. Le store notifie les composants 4. Les composants se re-rendent ## Redux Toolkit : Redux simplifié Redux classique nécessite beaucoup de code. **Redux Toolkit** (RTK) simplifie tout : ```bash npm install @reduxjs/toolkit react-redux ``` ## Créer un Slice Un **slice** regroupe l'état, les reducers et les actions pour une fonctionnalité : ```javascript // features/counter/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { // RTK utilise Immer : on peut "muter" directement state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; } } }); // Export des actions export const { increment, decrement, incrementByAmount } = counterSlice.actions; // Export du reducer export default counterSlice.reducer; ``` ## Configurer le Store ```javascript // app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer } }); ``` ## Connecter React au Store ```javascript // index.js ou App.js import { Provider } from 'react-redux'; import { store } from './app/store'; function App() { return ( ); } ``` ## Utiliser Redux dans un composant ```javascript // features/counter/Counter.js import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './counterSlice'; function Counter() { // Lire l'état const count = useSelector(state => state.counter.value); // Obtenir la fonction dispatch const dispatch = useDispatch(); return (

{count}

); } ``` ## Exemple complet : Panier d'achat ### Le Slice ```javascript // features/cart/cartSlice.js import { createSlice } from '@reduxjs/toolkit'; const cartSlice = createSlice({ name: 'cart', initialState: { items: [], total: 0 }, reducers: { addItem: (state, action) => { const existingItem = state.items.find( item => item.id === action.payload.id ); if (existingItem) { existingItem.quantity += 1; } else { state.items.push({ ...action.payload, quantity: 1 }); } state.total = state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); }, removeItem: (state, action) => { state.items = state.items.filter(item => item.id !== action.payload); state.total = state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); }, updateQuantity: (state, action) => { const { id, quantity } = action.payload; const item = state.items.find(item => item.id === id); if (item) { item.quantity = quantity; if (item.quantity <= 0) { state.items = state.items.filter(i => i.id !== id); } } state.total = state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); }, clearCart: (state) => { state.items = []; state.total = 0; } } }); export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions; export default cartSlice.reducer; ``` ### Les composants ```javascript // components/ProductCard.js import { useDispatch } from 'react-redux'; import { addItem } from '../features/cart/cartSlice'; function ProductCard({ product }) { const dispatch = useDispatch(); const handleAddToCart = () => { dispatch(addItem(product)); }; return (

{product.name}

{product.price} €

); } ``` ```javascript // components/Cart.js import { useSelector, useDispatch } from 'react-redux'; import { removeItem, updateQuantity, clearCart } from '../features/cart/cartSlice'; function Cart() { const { items, total } = useSelector(state => state.cart); const dispatch = useDispatch(); if (items.length === 0) { return

Panier vide

; } return (

Panier

{items.map(item => (
{item.name} dispatch(updateQuantity({ id: item.id, quantity: parseInt(e.target.value) }))} min="0" /> {item.price * item.quantity} €
))}

Total : {total} €

); } ``` ## Actions asynchrones avec createAsyncThunk Pour les appels API : ```javascript // features/users/usersSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // Action asynchrone export const fetchUsers = createAsyncThunk( 'users/fetchUsers', async () => { const response = await fetch('/api/users'); return response.json(); } ); const usersSlice = createSlice({ name: 'users', initialState: { items: [], loading: false, error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUsers.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchUsers.fulfilled, (state, action) => { state.loading = false; state.items = action.payload; }) .addCase(fetchUsers.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }); } }); export default usersSlice.reducer; ``` ```javascript // Utilisation function UserList() { const { items, loading, error } = useSelector(state => state.users); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchUsers()); }, [dispatch]); if (loading) return

Chargement...

; if (error) return

Erreur : {error}

; return ( ); } ``` ## Selectors Les **selectors** sont des fonctions pour extraire des données du store : ```javascript // features/cart/cartSelectors.js // Selector simple export const selectCartItems = state => state.cart.items; export const selectCartTotal = state => state.cart.total; // Selector calculé export const selectCartItemCount = state => state.cart.items.reduce((count, item) => count + item.quantity, 0); // Utilisation function CartIcon() { const itemCount = useSelector(selectCartItemCount); return 🛒 {itemCount}; } ``` ## Quand utiliser Redux vs Context ? ^ Critère ^ Context ^ Redux ^ | Données globales simples (thème, user) | ✅ | ❌ Overkill | | État partagé entre composants éloignés | ✅ | ✅ | | Logique de mise à jour complexe | ❌ | ✅ | | Actions asynchrones | Manuel | ✅ createAsyncThunk | | DevTools (time-travel debug) | ❌ | ✅ | | Large application | ❌ Performance | ✅ | ## Structure recommandée ``` src/ app/ store.js features/ cart/ cartSlice.js cartSelectors.js Cart.js CartIcon.js users/ usersSlice.js UserList.js products/ productsSlice.js ProductList.js ``` ## Points clés à retenir 1. **Store unique** : une seule source de vérité 2. **Actions** : décrivent ce qui s'est passé 3. **Reducers** : calculent le nouvel état 4. **Redux Toolkit** : simplifie Redux avec createSlice 5. **useSelector** : lire l'état 6. **useDispatch** : envoyer des actions 7. **createAsyncThunk** : actions asynchrones [[:15_training:module4-react-avance:custom-hooks|← Chapitre précédent]] | [[:15_training:module4-react-avance:start|Retour au module]] | [[:15_training:module5-architecture-smartmaker:start|Module suivant : Architecture SmartMaker →]]