comment gérer les états globaux dans une application React avec redux
Installation de React JS
pour commencer, nous devons d’abord créer le projet React JS. Nous avons plusieurs possibilités, voici quelques-uns :
Avec create-react-app
npx create-react-app myapp
Vite
On peut aussi créer le projet avec vite
npm create vite@latest
une fois, vous avez tapé cette commande, écrivez alors le nom du projet
myapp
et enfin, choisissez le type de projet, dans notre cas, c'est le react avec JavaScript.
Une fois tout est ok, ouvrer le dossier dans le terminal et installer tous les packets
cd myapp && npm install
Installation de Redux
une fois le projet est déjà créée, on peut ensuite installer redux dans notre projet.
Avec NPM
npm install redux react-redux redux-devtools-extension redux-thunk axios
avec yarn
yarn add redux react-redux redux-devtools-extension redux-thunk axios
Nous venons d’installer deux packages :
-
**redux**
: qui va nous permettre d’utiliser les fonctionnalités de redux dans notre projet -
**react-redux**
: vu que redux n’est pas exclusif à react, nous avons besoin de ce package qui va assurer l’interconnexion entre react et redux, il va nous permettre d’utiliser les éléments de redux dans react. -
**redux-devtools-extension**
va nous aider à voir nos states, actions et à nous déboguer avec l’extension redux devtools; -
**redux-thunk**
va nous permettre de faire des appels asynchrones vers une API. -
**axios**
pour faire des requêtes HTTP vers serveur, son alternative c’est l’API**fetch**
Nous avons alors redux dans notre projet et qui est d’utilisation, nous pouvons décortiquer le projet et voir comment utiliser redux dans notre projet.
node_modules
public
src
App.jsx
App.css
index.js
index.css
.gitignore
packages.json
packages-lock.json
readme.md
et notre package.json se présente ainsi
...
"dependencies": {
"axios": "^1.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.7.1",
"react-redux": "^8.0.5",
"react-router-hash-link": "^2.4.3",
"redux": "^4.2.0",
"@redux-devtools/extension": "^3.2.3",
"redux-thunk": "^2.4.2",
"...":"..."
},
"devDependencies": {
....
}
Structure de redux
Pour mieux structurer nos states globaux, nous avons 3 concepts que nous donne redux
- Le store
- le(s) reducer(s)
- le(s) action(s)
Le store
le store, c'est le magasin de nos states, c’est lui qui contient les states globaux de notre application, c’est, lui aussi, le point d’entrer (root) de notre redux;
pour créer le store, le point d’entrer de notre partie redux, nous devons créer un dossier app
dans le dossier src
cd src && mkdir app
Ce dossier app
contiendra la logique de nos états globaux, c.-à-d. Tout ce qu’on aura à manipuler avec redux ça serra dans ce dossier app
une fois le dossier est créé, nous allons créer le fichier pour le store, tout juste a la racine de ce dossier.
Ce repertoire va se presenter ainsi
...
src/
app
store.js
App.js
App.css
index.js
index.css
...
Alors créons notre store dans ce fichier store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { composeWithDevTools } from "@redux-devtools/extension";
import thunk from "redux-thunk";
const roorReducer = combineReducers({
/* ici nous allons placer le reducer de chaque module de notre app */
})
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
nous avons importé :
-
createStore
: la fonction qui va nous permettre de créer le store, le point d’entrer de redux; -
combineReducers
: cette fonction va combiner tous lesreducers
de nos modules en un seul reducer, qu’on peut alors enregistrer dans le store; -
applyMiddleware
: va nous permettre d’inscrire le middleware d’async (thunk) dans notre store. Il prend en paramètre le packagethunk
que nous avons installé ; -
composeWithDevTools
va nous permettre d’utiliser l’extension redux devtools, il prend en paramètre la fonctionapplyMiddleware
; -
thunk
: le package qui nous permettra de faire des requêtes asynchrones vers des API.
Le reducer
Nous venons de créer le store ( magasin) de notre application, il est temps de créer des reducers pour chaque module et les donner à notre rootReducer
.
Le reducer c’est la partie de redux qui contrôle le comportement de nos states (états) et qui spécifie comment les states doivent ou peuvent changer pour un module spécifique de notre application.
Dans notre application, nous aurons un ou plusieurs modules (par exemple l'authentification, blog, products… ).
Voici l’arborescence de notre projet, une fois, nous ajoutons des modules :
...
src/
app
store.js
auth/
auth.reducer.js
auth.action.js
auth.type.js
count/
count.reducer.js
count.action.js
count.type.js
App.js
App.css
index.js
index.css
...
Nous venons d’ajouter deux modules à notre projet :
- le counter
- Et l’authentification
l’Authentification
C’est quoi un reducer ?
Le reducer c’est une fonction qui prend 2 paramètres : le state et l’action, et à l’intérieur, on énumèrera chaque type d’action qu’on aura sur notre module, et comment on va modifier le state vis-a-vis de cette action.
Commençons par le module d’authentification.
Nous avons besoin d’un seul reducer pour gérer l’authentification de notre application, mais avec plusieurs actions.
Dans le fichier auth.type.js
ajoutons les types d’actions dons, nous allons utiliser :
// action types
export const LOGIN = "LOGIN";
export const AUTH_LOADING = "AUTH_LOADING";
export const AUTH_SIGNUP_SUCCESS = "AUTH_SIGNUP_SUCCESS";
export const AUTH_LOGIN_SUCCESS = "AUTH_LOGIN_SUCCESS";
export const AUTH_ERROR = "AUTH_ERROR";
Créons le reducer dans le fichier auth.reducer.js
import {
AUTH_ERROR, AUTH_LOADING,AUTH_LOGIN_SUCCESS, AUTH_SIGNUP_SUCCESS,
} from "./auth.type";
let user = localStorage.getItem("pae-user"))
user = typeof user == Object ? JSON.parse(localStorage.getItem("pae-user")) : null;
const initialState= {
user: user || null,
isLoading: false,
isSuccess: false,
isError: false,
errorMessage: "",
};
const authReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_LOADING:
return {
...state,
isLoading: true,
isSuccess: false,
isError: false,
user: null,
};
case AUTH_LOGIN_SUCCESS:
return {
...state,
isLoading: false,
isSuccess: true,
isError: false,
user: action.payload,
};
case AUTH_SIGNUP_SUCCESS:
return {
...state,
isLoading: false,
isSuccess: true,
isError: false,
user: action.payload,
};
case AUTH_ERROR:
return {
...state,
isLoading: false,
isSuccess: false,
isError: true,
errorMessage: action.payload,
};
default:
return state;
}
};
export default authReducer;
Expliquons le code
Premièrement, nous avons besoin de la valeur par défaut pour notre état (state). Dans notre cas, nous vérifions d’abord s’il y a d’informations dans le local Storage
pour l’utilisateur, si c’est le cas, nous le chargeons comme valeur initiale, dans le cas contraire, on utilise un objet
let user = localStorage.getItem("pae-user"))
user = typeof user == Object ? JSON.parse(localStorage.getItem("pae-user")) : null;
const initialState = {
user: user || null,
isLoading: false,
isSuccess: false,
isError: false,
errorMessage: "",
};
Si on le remarque bien pour notre state, nous avons un objet avec plusieurs clés :
-
user
: va contenir les informations de l’utilisateur qui va s’authentifier
Les autres clés, nous permettrons de savoir dans quelle situation nous sommes quand nous serons en train de faire des requêtes sur l’API :
-
isLoading
: il va nous aider à savoir si la requête est en cours et n’a pas encore donné un résultat ; -
isSuccess
: va nous informer si la requête a donné un résultat positif, c’est alors qu’on va charger la cléuser
des données qui viendront de l’API ; -
isError
: va nous informer si la requête a donné un résultat négatif, c’est alors qu’on va charger la cléerrorMessage
du message d’erreur qui viendra de l’API ;
Nous serons en train de modifier ces clés de notre state suivant le type d’action que nous allons déclencher dans le reducer
Enregistrons notre reducer dans le store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { composeWithDevTools } from "@redux-devtools/extension";
import thunk from "redux-thunk";
import authReducer from "./auth/auth.reducer"
const roorReducer = combineReducers({
auth: authReducer
})
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
Actions
Nous pouvons alors créer des fonctions qu’on serra en train d’appeler pour déclencher ces actions qu’on écoute dans notre reducer. Les actions vont être créées dans le dossier auth.action.js
Nous aurons deux types d’actions dans ce fichier.
-
Les actions directement lies à notre reducer, qui vont passer au reducer une et permettre au reducer de modifier le state suivant cette valeur
Les fonctions d’actions auront tous cette forme
const nomActionFunc = (data) => ({ type: TYPE_ACTION payload: data, });
c’est une fonction qui renvoi un objet avec deux clés
-
type
: le type d’action à déclencher -
payload
: la nouvelle donnée qu’on va ajouter, éditer dans le state global
-
Voici alors les fonctions d’actions dont nous aurons besoin pour la partie d’authentification :
auth.action.js
const authLoading = () => ({
type: AUTH_LOADING,
});
const signupSuccess = (user) => ({
type: AUTH_SIGNUP_SUCCESS,
payload: user,
});
const loginSuccess = (user) => ({
type: AUTH_LOGIN_SUCCESS,
payload: user,
});
const authError = (error) => ({
type: AUTH_ERROR,
payload: error,
});
Vu que nous allons faire des requêtes asynchrones, nous n’allons déclencher directement ces actions, nous aurons besoin d’une autre fonction, qui va déclencher (dispatcher
) l’action suivant la situation de la requête à l’API
Voici le cas du login et le register (sign up)
export const login = (email, password) => {
return async (dispatch) => {
dispatch(authLoading());
try {
const res = await axios.post(`${BACKEND_API}/api/v1/login`,{email, password});
dispatch(loginSuccess(res.data));
localStorage.setItem("user",JSON.stringify(res.data && res.data.data));
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
localStorage.removeItem("user");
return dispatch(authError(message.toString()));
}
};
};
export const signup = (email,password) => {
return async (dispatch) => {
dispatch(authLoading());
try {
const res = await axios.post(`${BACKEND_API}/api/v1/register`,{email, password});
dispatch(signupSuccess(res.data && res.data.data));
localStorage.setItem("user", JSON.stringify(res.data.token));
console.log(res.data);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return dispatch(authError(message));
}
};
};
Dans chaque fonction, nous avons trois situations :
- Le lancement de la requête : dès que nous avons envoyé la requête vers l’API, nous dispatchons la fonction
authLoading()
qui va déclencher l’action**AUTH_LOADING**
et le reducer va changer leisLoading
a true ; - Dès que la requête marche bien et renvoie une réussite avec des données (data), nous allons dispatcher la fonction
loginSuccess()
pour le cas de login etsignupSuccess()
pour le cas de sign up en leur passant les informations de l’utilisateur venu de l’API, ça peut être un token ou les identifiants de l’utilisateur. Ces deux fonctions vont déclencher respectivement les actions**AUTH_LOGIN_SUCCESS**
etAUTH_SIGNUP_SUCCESS
qui vont permettre au reducer de prendre les informations dans le payload et de mettre dans le state à la clé user ; - En cas d’erreur, nous allons dispatcher la fonction authError() en lui passant le message d’erreur, qui peut être soit l’erreur due au serveur, l’erreur liée au réseau, l’erreur de connexion.
Utilisation des états, states globaux dans la partie react.js
Pour manipuler les états, states globaux (les states gérés par redux), nous avons besoin d’utiliser les hooks du package **react-redux**
-
**useSelector()**
: pour utiliser la valeur, le contenu d’un ou plusieurs états globaux ; -
**useDispatch()**
: pour déclencher une fonction d’action, une fonction que nous avons défini dans redux pour déclencher une action pour un état (state).
-
useDispatch()
useDispatch()
est un hook qui nous retourne une fonction, généralement appelé dispatch qui va nous aider à déclencher une action pour modifier l’état global de notre application.Dans le cas de notre exercice d’authentification, on peut implémenter le signup ou la création d’un compte de cette manière :
Premièrement, nous devons englober notre application dans un composant appelé Provider pour permettre à chaque composant d’accéder aux données de redux
index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import store from './app/store'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </React.StrictMode> );
Nous avons importé :
- `store` : depuis notre dossier app
- `Provider` : depuis le package react-redux
Ensuite, nous pouvons alors éditer notre composant de création de compte
```jsx
import React, {useState} from 'react'
import { useDispatch } from 'react-redux'
import { register } from '../app/auth/auth.action';
const RegisterPage = () => {
const dispatch = useDispatch()
const [userInput, setUserInput] = useState({
email: '',password:'', confirmPassword: ''
})
const handleSubmit = () => {
e.preventDefault()
if(userInput.password === userInput.confirmPassword){
dispatch(login(userInput.email, userInput.password)
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<label>email</label>
<input name='email' onChange={
(e) => setUserInput({...userInput, email: e.target.value})} />
<label>password</label>
<input name='password' type='password' onChange={
(e) => setUserInput({...userInput, password: e.target.value})} />
<label>confirm password</label>
<input name='confirmPassword' type='password' onChange={
(e) => setUserInput({...userInput, confirmPassword: e.target.value})} />
<button type='submit'>Enregistrer</button>
</form>
}
```
Top comments (0)