DEV Community

Cover image for Prennez en main redux
Cédric Karungu for KADEA ACADEMY

Posted on • Edited on

Prennez en main redux

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
Enter fullscreen mode Exit fullscreen mode

Vite

On peut aussi créer le projet avec vite

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

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

myapp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

avec yarn

yarn add redux react-redux redux-devtools-extension redux-thunk axios
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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": {
            ....
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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))
);
Enter fullscreen mode Exit fullscreen mode

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 :

...
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
...
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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: "",
};
Enter fullscreen mode Exit fullscreen mode

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))
);
Enter fullscreen mode Exit fullscreen mode

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

    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


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,
});
Enter fullscreen mode Exit fullscreen mode

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()));
        }
    };
};
Enter fullscreen mode Exit fullscreen mode
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));
        }
    };
};
Enter fullscreen mode Exit fullscreen mode

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

    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
Enter fullscreen mode Exit fullscreen mode
```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>
}
```
Enter fullscreen mode Exit fullscreen mode

Top comments (0)