Laboratoire 3.2: Transmission de données (Partie 2)
Ce laboratoire est la suite du précédent, mais les techniques que nous allons voir sont plus compliquées. Il est conseillé de le faire en une fois.
Donc, fermez l'onglet 9gag, mettez votre téléphone en mode avion, chargez votre ordinateur et coupez la musique !

3. Via un store
Cette 3ème approche vise à régler un gros problème avec les propriétés: la hiérarchie des composants. Imaginez la situation suivante: un 1er composant appelle un 2ème composant. Ce 2ème composant appelle un 3ème composant, etc. Dans notre situation, nous devons passer une variable du composant 1 au dernier composant (imaginons le 5ème). Voyez-vous le problème ? Schématisons la situation:
Composant1 --> Composant2 --> Composant3 --> Composant4 --> Composant5
Maintenant, si on passe la variable du Composant1 au Composant5, nous aurons ceci:
Composant1(variable) --> Composant2(variable) --> ... --> Composant5(variable)
Maintenant le problème semble évident. Pour passer notre variable du Composant1 au Composant5, nous sommes obligés de le passer à toute la hiérarchie. Ce qui va créer un couplage dans nos composants (ce qu'il faut absolument éviter, vous vous en doutez).
Les stores ou contextes vont nous permettent de passer ces informations dans un "point" accessible de tous. En schématisant:
Composant1 <--> Store/Contexte <--> Composant5
Ici, Composant1 et Composant5 lisent et écrivent dans le Store/Contexte. Mais nous pourrions imaginer une infinité de composants s'abonnant au Store/Contexte ! Ce qui est très pratique si toute votre interface s'adapte en fonction du choix d'un thème, par exemple !
Nous ne sommes plus donc plus obligés de traverser toute la hiérarchie avec nos variables. Il existe deux approches:
Nous passerons rapidement sur la première, mais nous prendrons plus de temps pour la seconde qui se révèlera rapidement plus complexe.
createContext()
Une première solution légère consiste à avoir une "sorte" de variable partagée via un Context. Il suffit d'appliquer les étapes suivantes:
- Créer le contexte
- Fournir le contexte aux composants
- Connecter un composant au contexte
Créer le contexte
Pour créer un contexte, nous utiliserons la fonction createContext(defaultValue). Le seul paramètre de la fonction permet d'avoir une valeur par défaut quand aucun Provider n'est fourni au composant utilisant le contexte (nous le détaillerons à l'étape suivante). Pour créer notre contexte il suffit de faire:
import { createContext } from 'react';
const ThemeContext = createContext('light');
export default ThemeContext
Fournir le contexte aux composants
Pour fournir ce contexte, nous allons utiliser le Provider du contexte. Ainsi, ce contexte sera à disposition de tous les composants enfants du Provider. Le Provider a besoin d'une propriété value qui contiendra la valeur à partager.
import ThemeContext from './context/Theme';
function App(){
return (
<ThemeContext.Provider value='light'>
{/*Le ThemeContext devient accessible à n'importe quel composant*/}
</ThemeContext.Provider>
)
}
export default App;
Si vous ne fournissez aucune valeur, la valeur par défaut sera celle utilisée lors de la création du contexte.
Connecter un composant au contexte
Pour connecter un composant au contexte, il suffit d'appeler le hook useContext(SomeContext). La fonction prend un seul paramètre: le contexte. Ainsi React pourra trouver le Provider contenant la valeur désirée.
import { useContext } from "react"
import ThemeContext from "../context/Theme";
function Button({onClick}){
const theme = useContext(ThemeContext)
const backgroundColor = theme === 'light' ? 'white' : 'black';
const color = theme === 'light' ? 'black' : 'white';
return (
<button onClick={onClick} style={{backgroundColor, color}}>Bouton</button>
)
}
export default Button;
Maintenant que notre composant s'est abonné au contexte, chaque fois que ce dernier sera changé le composant sera re-rendu (comme s'il s'agissait d'un état) !
Mise à jour du contexte
Avec le code fourni plus haut, il n'est pas possible de modifier le contexte. Pour modifier le contexte, nous allons en réalité créer un état qui sera passé comme valeur au contexte.
import {useState} from 'react';
import ThemeContext from './context/Theme';
import Button from './components/Button.jsx';
function App() {
const [theme, setTheme] = useState('dark'); // création d'un état
return (
<ThemeContext.Provider value={theme}> {/* l'état est passé au contexte */}
{/*reste du code*/}
<Button onClick={() => {
setTheme(theme === 'light' ? 'dark' : 'light');
/*
La fonction va changer l'état. La modification de l'état va changer la valeur du contexte.
Le changement de valeur du contexte va provoquer une mise à jour de tous les composants
abonnés à ce contexte
*/
}}>
</Button>
</ThemeContext.Provider>
);
}
export default App;
Redux
Redux permet de gérer l'état d'une application Javascript (autrement dit un store). Redux peut très bien fonctionner avec React (avec React-Redux), NodeJS, Angular, Vue, etc. Redux amène une série de concepts et React-Redux permet une intégration facile avec React. Pour pouvoir les utiliser, utilisez la commande suivante:
npm install react-redux @reduxjs/toolkit
Redux utilise le paradigme fonctionnel pour la gestion de l'état. C'est-à-dire que l'état ne peut être modifié et chaque action "dispatchée" va créer un nouvel état qui sera sauvegardé. Quand une action est reçue par le store, elle passe par une série de reducers qui va permettre de recréer un nouveau store. Dans Redux, le store peut être vu comme un objet où chaque propriété de l'objet est liée à un reducer.
Dans l'exemple ci-dessous, vous avez une représentation schématique du fonctionnement de Redux. Dans les faits, vous vous doutez que le fonctionnement réel est plus complexe. Le but de ce code est de vous permettre une meilleure visualisation. La fonction dispatch de redux permet d'envoyer une "action" qui sera lue par le reducer et engendrera un nouvel état !
let store = {
listeProduits: [/*une liste de produits*/],
nextIDProduit: 3
}
function reducer(state, action){
switch action.name:
case "addProduit":
const newState = {...state};
newState.listeProduits.push({
...action.payload,
id: nextIDProduit
}) // on supposera que action.payload contient le produit à ajouter
newState.nextIDProduit += 1
return newState;
default: //si on envoie une action inconnue à dispatch, la fonction renverra le même état
return state;
}
//Une représentation SIMPLIFIEE de dispatch
function dispatch(action){
store = reducer(store, action)
}
Le gros avantage de Redux sur les Context est qu'il permet une meilleure gestion de l'état grâce à un meilleur store et une logique plus divisée. Avec les Context, vous serez rapidement obligés de les multiplier et vous risquez de vous y perdre.
React-Redux
Ce module permet de faire le lien entre React et Redux. Il vient également avec une série d'outils permettant de créer bien plus facilement un store. Nous suivrons les explications de la documentation officielle. Elles seront accompagnées de commentaires supplémentaires pour vous aider à comprendre.
configureStore()
Dans un premier temps, nous aurons besoin de créer la pièce centrale: le store. La méthode configureStore(reducer) (de Redux) permet de créer le store avec son Reducer (une fonction qui permet de générer un nouvel état).
import { configureStore } from '@reduxjs/toolkit'
export default configureStore({
reducer: {},
})
Provider pour le store
Maintenant que le store est créé, il faut le rendre accessible à notre application pour que les composants puissent s'y abonner. Nous allons importer le Provider de react-redux à cet effet:
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
Dans un premier temps, nous importons le store précédemment créé, idem pour le Provider de react-redux. Ensuite, nous engloblons le composant App pour rendre le store accessible à ce composant, mais également à tous ses enfants.
Création des slices
Les slices sont un moyen de mieux diviser les reducers. Actuellement, dans Redux, vous n'avez qu'un seul gros reducer. React-redux propose une solution à ce problème en proposant des slices. Le principe est de créer un reducer par propriété de l'objet contenu dans le store.
import { createSlice } from '@reduxjs/toolkit'
export const produitsSlice = createSlice({
name: 'listeProduits',
initialState: {
produits: [],
nextIdProduit: 0
},
reducers: {
addProduit: (state, action) => {
//Exemple action = {payload: {nom, prix}}
state.produits.push({
...action.payload,
id: state.nextIdProduit
});
state.nextIdProduit += 1
},
//je rajoute une action possible
removeProduit: (state, action) => {
//Exemple: action = {payload: {id: 2}}
state.produits = state.produits.filter(elem => action.payload.id !== elem.id)
}
},
})
export const { addProduit, removeProduit } = produitsSlice.actions
export default produitsSlice.reducer
Normalement, ce code devrait vous "embêter". En effet, l'état est censé être immuable. Hors, nous le modifions dans les reducers. En réalité, @reduxjs/toolkit utilise une librairie qui vous permet d'écrire les modifications dans une copie et détectera les modifications pour générer le nouvel état.
Ajout des slices au reducer
Maintenant, nous allons ajouter ce slice dans le reducer, pour indiquer à quelle propriété il est lié.
import { configureStore } from '@reduxjs/toolkit'
import produitReducer from './slice/produit.js'
export default configureStore({
reducer: {
listeProduits: produitReducer,
}
})