Prennez en main redux
Tags: javascript, redux, react, setup
Published: 2023-01-26 11:45:30


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

```jsx
npx create-react-app myapp
```

### Vite

On peut aussi créer le projet avec vite

```jsx
npm create vite@latest
```

une fois, vous avez tapé cette commande, écrivez alors le nom du projet

```jsx
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

```bash
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 

```bash
npm install redux react-redux redux-devtools-extension redux-thunk axios
```

avec yarn 

```bash
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.

```bash
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

```jsx
...

  "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`

```bash
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

```bash
...
src/
	app
			store.js
	App.js
	App.css
	index.js
	index.css
...
```

Alors créons notre store dans ce fichier `store.js`

```jsx
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 les `reducers` 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 package `thunk` que nous avons installé ;
- `composeWithDevTools`  va nous permettre d’utiliser l’extension redux devtools, il prend en paramètre la fonction `applyMiddleware` ;
- `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 : 

```bash
...
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 :

```jsx
// 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`

```tsx
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

```bash
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

```jsx
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.

1. 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
    
    ```jsx
    const nomActionFunc = (data) => ({
        type: TYPE_ACTION
        payload: data,
    });
    ```
    
    c’est une fonction qui renvoi un objet avec deux clés
    
    1. `type` : le type d’action à déclencher
    2. `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`

```jsx

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)

 

```jsx
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()));
        }
    };
};
```

```jsx
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 :

1. 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 le `isLoading` a true ;
2. 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 et `signupSuccess()` 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**` et   **`AUTH_SIGNUP_SUCCESS`** qui vont permettre au reducer de prendre les informations dans le payload et de mettre dans le state  à la clé user ;
3. 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).

1. 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
    
    ```jsx
    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>
    }
    ```