1°/ Principes de React

1.1°/ A l'origine

  • React est sorti en 2011 un peu après AngularJS (NB: angular v2 est relativement différent)
  • Contrairement à Angular, React n'est pas un environnement de développement MVC : c'est juste une bibliothèque permettant de gérer la partie vue d'une application.
  • React se base sur la définition de composants réutilisables et composables, chacun pouvant avoir un état local, transmissible à ses descendants (principes des props), et pouvant également émettre des événements personnalisés.
  • L'objectif est d'utiliser ces composants pour créer une SPA qui intègre la bibliothèque ReactJS pour gérer leur création et leur "montage" dans le DOM.
  • Pour des raisons de performances notamment lors de changements dans l'état des composants, React utilise un DOM virtuel.
  • Au final, on s'aperçoit que Vuejs n'a rien inventé puisque ce sont les même principes fondamentaux que React, mais implémentés différemment.
  • Les points de dissemblance les plus remarquables avec vuejs sont :
    • dans la définition du composant : on écrit une classe qui hérite de React.Component et qui contient aussi bien les données locales, que les fonctions de contrôle et la fonction qui crée le template (cf. point suivant).
    • dans la façon d'écrire le template d'un composant : React utilise un langage nommé JSX, qui est du HTML avec des inclusions de JS entre { }.
  • Cette façon d'écrire les composants à l'avantage d'empaqueter TOUTE la définition du composant dans une seule classe, même si cette dernière peut bien entendu importer des fonctionnalités se trouvant dans d'autres modules.
  • En revanche, la définition d'une classe apporte son lot de "contraintes" et "pièges" dus aux différentes façon de définir et appeler des fonctions en JS.
  • Pour résumer, il faut connaître parfaitement comment se comporte le mot-clé this selon le contexte d'exécution et ce n'est pas simple pour un débutant.
  • Il y a aussi des contraintes sur la façon de modifier l'état local du composant, ainsi que toutes les subtilités liées à JSX.
  • C'est pourquoi React n'est pas d'un abord aussi aisé que vuejs, qui a bien moins de contraintes et pièges.
  • Fort heureusement, on peut se débrouiller dans beaucoup de cas en suivant des "recettes de cuisines", même si on ne comprend pas ce que l'on écrit.
  • NB : ces problèmes sont illustrés dans la démonstration.

 

  • Voici un exemple de composant affichant un titre issu d'une props, et un bouton permettant d'incrémenter une valeur.
import React from "react";
class MyCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {            
            counter: 0
        };
        this.handleClick= this.handleClick.bind(this);
    }
    handleClick() {
        this.setState({counter: this.counter+1})
    }
    render() {
        return (
            <div>
                <p>{this.props.title}</p>
                <button onClick={this.handleClick}>{this.counter}</button>
            </div>
        )
    }
}
  • Dans cet exemple, on remarque notamment l'appel à bind(this), qui permet de "relier" handleClick() au contexte d'exécution de l'objet courant, et donc d'accéder à son state.
  • On remarque également la fonction render() qui a pour but de créer le template en JSX. On remarque à ce sujet que contrairement à vue,js où le this n'est pas nécessaire dans le template, en React, il est obligatoire pour accéder aux membres du composant.
  • Enfin, on remarque que l'on ne modifie pas directement le state local, mais en appelant setState(). Sinon, le DOM ne serait pas rafraîchit.
  • En conclusion, on est pas très loin de ce que l'on pourrait écrire en vue v2, mais avec plus de contraintes à cause de l'utilisation des classes.
  • Mais depuis la v16.8 en 2019, les hooks sont apparus dans React, ce qui permet de simplifier grandement l'écriture, qui est sensiblement la même que celle de vue 3 (qui est apparu juste après).

1.2°/ Avec les hooks.

  • Pour faire simple, la syntaxe des hooks permet d'écrire un composant sous la forme d'une fonction qui elle même appelle et définit d'autres fonctions.
  • Parmi ces fonctions, certaines sont directement fournies par React, par ex. useState() et useEffect(), afin de définir des variables locales réactives, et des sortes de watchers, etc.
  • Le très gros avantage des hooks est de supprimer tous les pièges et contraintes liés à l'utilisation de this, puisque dans une fonction, on n'utilise pas this.
  • Les codes sont donc beaucoup plus compacts et lisibles, façon vue 3.
  • Voici le même exemple du composant de la section 1.1, mais avec des hooks :
import { useState } from 'react'
function MyCounter(props) {
    const [counter, setCounter] = useState(0)
    function handleClick() {
        setCounter(counter+1)
    }
    return (
        <div>
            <p>{props.title}</p>
            <button onClick={handleClick}>{counter}</button>
        </div>
    )
}
export default MyCounter
  • On peut remarquer que useState() est un peu comme la fonction ref() de vuejs, excepté qu'elle renvoie un couple valeur/fonction. La fonction est utilisée pour mettre à jour la valeur, ce qui implique un rafraîchissement du DOM. La variable counter n'est donc pas observée au sens de vuejs, puisqu'il faut passer par une fonction pour obtenir une mise à jour du DOM. C'est moins "direct".
  • On voit également que this a totalement disparu !
  • En conclusion, le composant devient vraiment simple à écrire, quasi autant qu'en vue 3.

 

2°/ Créer un projet React avec Vite

  • Les principes de création sont les mêmes que pour une application vue.

2.1°/ Création initiale

  • La façon la plus simple de commencer un projet avec vite est de lancer la commande :
npm create vite@latest
  • Ensuite, il suffit d'indiquer le nom du répertoire racine du projet, quel type d'application : React, et quel langage (par ex, javascript).
  • Comme indiqué dans le résultat, il faut ensuite aller dans le répertoire du projet et installer tous les paquets node : npm install.

 

  • Il est fréquent que l'on utilise aussi bien des fichiers js que jsx dans une application react.
  • Pour éviter que Vite émette des erreurs, il faut modifier vite.config.js comme suivant :
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  esbuild: {
    loader: 'jsx',
  },
  optimizeDeps: {
    esbuildOptions: {
      loader: {
        '.js': 'jsx',
      },
    },
  },
})

 

 

2.2°/ Installer les plugins

  • Le problème de cette méthode est qu'elle ne permet pas d'intégrer directement les plugins tels que react-router-dom et redux (c.a.d. un gestion de store).

Remarque : le plugin react-router-dom permet de fiare le routage dans des applis web. Pour faire de même dans des applis mobiles avec ract-native, il faut installer rect-router-native.

  • Cela dit, il suffit de les installer puis de modifier le fichier de "lancement" de l'application.
  • Dans le répertoire racine du projet, taper :
npm install react-router-dom
npm install react-redux
npm install @reduxjs/toolkit
  • Normalement npm ls devrait afficher quelque chose du type :
npm ls
myproj@0.0.0 /home/login/myproj
├── @reduxjs/toolkit@1.9.3
├── @types/react-dom@18.0.11
├── @types/react@18.0.28
├── @vitejs/plugin-react@3.1.0
├── react-dom@18.2.0
├── react-redux@8.0.5
├── react-router-dom@6.9.0
├── react@18.2.0
└── vite@4.2.0

2.3°/ mettre en place react-router-dom

  • react-router-dom propose globalement deux syntaxes de définitions des routes, dont une qui ressemble à celle de vue-router.
  • Il y a cependant des différences fondamentales dont le fait que react-router :
    • n'utilise pas le principe d'emplacement nommé, ce qui ne permet pas d'afficher plusieurs composants à différents emplacements, grâce à une seule route,
    • le composant principal App est généralement lui-même routé,
    • ne permet pas de transformer des paramètres de route en props.
  • Pour le reste, les possibilité sont à peu près similaires, même si les noms et principes d'implémentation sont différents.

 

  • Pour commencer, il est conseillé de créer un fichier router/index.jsx avec dedans :
import {createBrowserRouter} from "react-router-dom";
// import des composants.
...
// définition des routes, par ex:
const router = createBrowserRouter([
  {
    path:'/',
    element: <App />,
    children: [
      { path: 'welcome', element: <Welcome /> }
    ]
  }
])

export default router

 

  • Ensuite, il faut mettre à disposition le router à toute l'application vue.
  • Pour cela, il suffit de modifier src/main.js comme suivant :
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import {RouterProvider} from 'react-router-dom';
import router from "./router";

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
            <RouterProvider router={router}/>
    </React.StrictMode>,
)

 

  • Pour faire le routage, il existe en gros 2 principes, comme dans vue-router :
    • utiliser la balise <Link> et son attribut to pour créer un lien
    • utiliser le hook useNavigate() pour obtenir une fonction que l'on peut appeler pour suivre un route (l'équivalent de $router.push())
  • Pour qu'un composant parent puisse affiche son composant fils, on utilise la balise <Outlet>.
  • Pour qu'un composant routé accède aux paramètres de sa route, on utilise le hook useParams().

 

NB : ces différents points sont abordés dans la démonstration.

 

2.4°/ Mettre en place Redux

ATTENTION ! Redux est une "usine à gaz" comparé à pinia, surtout si on veut l'utiliser avec des composants sans hooks, ou lorsque l'on veut un store qui va chercher des données auprès d'une API. Il reste cependant une "référence" dans le monde react, donc à connaître.

  • Redux repose du vocabulaire un peu étrange pour décrire l'architecture d'un store.
  • On commence par écrire des slices, grâce à la fonction createSlice(). Ce sont un peu comme des modules dans vuex.
  • Un slice contient un champ initialState avec des variables constituant le state, et un champ reducers avec des fonctions manipulant le state, donc l'équivalent des actions dans pinia.
  • A partir du nom de ces fonctions, createSlice() va créer une liste d'objets "actions" (!! notion différente de celle de vuex/pinia !!), qui sont utilisés comme paramètre par la fonction dispatch() lorsque l'on veut appeler ces fonctions. C'est donc le même principe d'appel indirect aux mutations que dans vuex.
  • Cette liste d'actions dot donc être exportée afin d'être mise à disposition des composants désirant manipuler le slice.
  • D'autre part, les fonctions reducer doivent également être exportées, afin d'être assemblée dans l'objet store global.

Exemple basique avec un compteur, dans un fichier counter.slice.js :

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
    name: 'counter',
    initialState: {
        value: 0
    },
    reducers: {
        increment: state => { state.value += 1 },
        decrement: state => { state.value -= 1 },
    }
})
// Action creators are generated for each case reducer function
export const { increment, decrement} = counterSlice.actions
export default counterSlice.reducer

 

  • Pour créer le store global, on utilise la fonction configureStore(), qui reçoit un objet en paramètre.
  • Cet objet contient un champ reducer, dont la valeur est une liste recensant les reduces exportés par les slices.

 Exemple avec le slice counter :

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counter.slice'
export default configureStore({
  reducer: {
    counter: counterReducer,
  },
})

 

  • Dans les composants, on utilise le hook :
    • useSelector() afin de récupérer un accès à une variable du state d'un slice
    • useDispatch() pour avoir accès à la fonction dispatch()
  • Les actions sont simplement importées.

Par exemple :

import { useSelector, useDispatch } from 'react-redux'
import { increment} from '../store/counter.slice'
function TestCounter() {
    const count = useSelector(state => state.counter.value)
    const dispatch = useDispatch()
    return (
        <div>
                <p>{count}</p>
                <button onClick={() => dispatch(increment)}>Click</button>
        </div>
    )
}
export default TestCount

 

Remarques :

  • Pour écrire des traitement asynchrones qui interagissent avec le store, on utilise la notion de thunk, et la fonction createAsyncThunk() qui permet de les créer et de les interfacer facilement avec le store.
  • Un exemple est donné dans le démonstration.

 

3°/ Démonstration

  • Le code de démonstration est téléchargeable [ ici ]
  • Il contient uniquement le répertoire src.
  • Explorer le code de App : on remarque la structuration du code et l'utilisation de useState() et useEffect(), qui donne un code assez similaire à du vue 3.
  • Explorer le code de router/index.js : c'est relativement semblable à ce que l'on peut écrire en vue-router, même sur la syntaxe des paramètres des routes.
  • Explorer le code des composants Users et UsersWithoutHooks pour illustrer la simplicité de développement avec vs sans hooks.
  • Dans Users, on remarque l'utilisation de useParams() pour avoir accès au paramètre de la route (en l'occurrence le mot hello).
  • Dans UsersWithoutHooks, on remarque que l'on ne peut pas accéder directement aux paramètre, puisque les hooks sont interdits dans les classes. On utilise donc une astuce "pourrie" qui consiste à encapsuler le composant dans un autre, ce dernier étant créé avec la nouvelle syntaxe et permettant donc d'utiliser useParams(). Cette astuce n'est évidemment utile que lorsque l'on veut réutiliser des "anciens" composants.
  • Dans ce même composant, on remarque le problème liés aux callbacks utilisés lors de certains événements, selon qu'ils sont appelés directement ou via une fonction fléchée.
  • Par exemple handleAdd() est appelée en cas de click, mais du coup, hors du contexte de l'objet représentant le composant. Il n'y a donc pas la bonne valeur de this. C'est pourquoi dans le constructeur, on relie "de force" handleAdd() à this du composant, grâce à bind().
  • En revanche, handleRemove() est appelée grâce à une fonction fléchée. Or, une fonction fléchée est toujours reliée au contexte où elle est définie, donc son this correspond au composant. Par transitivité, le contexte d'appel de handleRemove() est le même que celui de la fonction fléchée, donc this a bien la bonne valeur.
  • Pour illustrer ce qui arrive en cas de this invalide, clique sur le bouton du bas. Il appelle la fonction log() qui elle-même essaye d'accéder à this.state. Comme this n'a pas la bonne valeur, il n'y a pas d'attribut state, donc la console affiche une erreur. Pour éviter cette erreur, il faudrait soit appeler log() via une fonction fléchée lors du clic, soit la relier de force au this du composant.
  • Explorer le contenu des différents slices. Celui de towns.slice.js permet d'illustrer le concept de thunk et de la gestion "usine à gaz" du cycle de vie du thunk, pour tenir compte du caractère asynchrone du traitement.
  • En revanche, les autres slices sont relativement simples et pas très éloignés de vuex.
  • Explorer le contenu du composant Towns. On remarque que le code est très simple pour récupérer la liste de villes : toute la complexité est dans le slice.