Aller au contenu

c

État des composants, gestionnaires d'événements

Reprenons avec React.

On commence avec un nouvel exemple :

const Hello = (props) => {
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
    </div>
  )
}

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
    </div>
  )
}

Fonctions d'assistance aux composants

Développons notre composant Bonjour afin qu'il devine l'année de naissance de la personne accueillie :

const Hello = (props) => {
  const bornYear = () => {    const yearNow = new Date().getFullYear()    return yearNow - props.age  }
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>    </div>
  )
}

La logique pour deviner l'année de naissance est séparée en une fonction qui est appelée lorsque le composant est rendu.

L'âge de la personne n'a pas besoin d'être passé en tant que paramètre de la fonction, car il peut accéder directement à toutes les props passés au composant.

Si nous examinons attentivement notre code actuel, nous remarquerons que la fonction d'assistance (helper) est en fait définie à l'intérieur d'une autre fonction qui définit le comportement de notre composant. En programmation Java, définir une fonction à l'intérieur d'une autre est complexe et fastidieux, donc pas si courant. En JavaScript, cependant, définir des fonctions dans des fonctions est une technique couramment utilisée.

Déstructuration

Avant d'aller plus loin, nous allons jeter un oeil à une petite fonctionnalité mais utile du langage JavaScript qui a été ajoutée dans la spécification ES6, qui nous permet de déstructurer des valeurs des objets et des tableaux lors de l'affectation.

Dans notre code précédent, nous devions référencer les données transmises à notre composant en tant que props.name et props.age. De ces deux expressions, nous avons dû répéter props.age deux fois dans notre code.

Puisque props est un objet

props = {
  name: 'Arto Hellas',
  age: 35,
}

nous pouvons rationaliser notre composant en affectant les valeurs des propriétés directement dans deux variables name et age que nous pouvons ensuite utiliser dans notre code :

const Hello = (props) => {
  const name = props.name  const age = props.age
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Notez que nous avons également utilisé la syntaxe plus compacte pour les fonctions fléchées lors de la définition de la fonction bornYear. Comme mentionné précédemment, si une fonction fléchée consiste en une seule expression, le corps de la fonction n'a pas besoin d'être écrit à l'intérieur d'accolades. Dans cette forme plus compacte, la fonction renvoie simplement le résultat de l'expression unique.

Pour récapituler, les deux définitions de fonction présentées ci-dessous sont équivalentes :

const bornYear = () => new Date().getFullYear() - age

const bornYear = () => {
  return new Date().getFullYear() - age
}

La déstructuration rend l'assignation des variables encore plus facile, puisque nous pouvons l'utiliser pour extraire et regrouper les valeurs des propriétés d'un objet dans des variables distinctes :

const Hello = (props) => {
  const { name, age } = props  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Si l'objet que nous destructurons a les valeurs

props = {
  name: 'Arto Hellas',
  age: 35,
}

l'expression const { name, age } = props attribue les valeurs 'Arto Hellas' à name et 35 à age.

Nous pouvons aller plus loin dans la déstructuration :

const Hello = ({ name, age }) => {  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>
        Hello {name}, you are {age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Les props passées au composant sont maintenant directement déstructurées dans les variables name et age.

Cela signifie qu'au lieu d'affecter l'intégralité de l'objet props dans une variable appelée props, puis d'affecter ses propriétés dans les variables name et age

const Hello = (props) => {
  const { name, age } = props
  ...
}

nous attribuons les valeurs des propriétés directement aux variables en déstructurant l'objet props qui est passé à la fonction du composant en tant que paramètre :

const Hello = ({ name, age }) => {...}

Re-rendu de la page

Jusqu'à présent, toutes nos applications ont été telles que leur apparence reste la même après le rendu initial. Et si on voulait créer un compteur dont la valeur augmente en fonction du temps ou au clic d'un bouton ?

Commençons par ce qui suit. Le fichier App.js devient :

const App = (props) => {
  const {counter} = props
  return (
    <div>{counter}</div>
  )
}

export default App

Et le fichier main.js devient :

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'

let counter = 1

ReactDOM.createRoot(document.getElementById('root')).render(
  <App counter={counter} />
)

Le composant App reçoit la valeur du compteur via la propriété counter. Ce composant restitue la valeur à l'écran. Que se passe-t-il lorsque la valeur de counter change ? Même si nous devions ajouter ce qui suit

counter += 1

le composant ne sera pas rendu à nouveau. Nous pouvons obtenir un nouveau rendu du en appelant la méthode render une deuxième fois, par ex. de la manière suivante :

let counter = 1

const refresh = () => {
  ReactDOM.createRoot(document.getElementById('root')).render(
    <App counter={counter} />
  )
}

refresh()
counter += 1
refresh()
counter += 1
refresh()

La commande de re-rendu a été intégrée à la fonction refresh pour réduire la quantité de code copié-collé.

Maintenant, le composant rend trois fois, d'abord avec la valeur 1, puis 2 et enfin 3. Cependant, les valeurs 1 et 2 sont affichées à l'écran pendant une durée si courte qu'elles ne peuvent pas être remarqué.

Nous pouvons implémenter des fonctionnalités légèrement plus intéressantes en recréant et en incrémentant le compteur toutes les secondes en utilisant setInterval :

setInterval(() => {
  refresh()
  counter += 1
}, 1000)

Faire des appels répétés de la méthode render n'est pas la méthode recommandée pour rafraichir nos composants. Nous présenterons plus tard une meilleure façon d'obtenir cet effet.

Composant stateful

Jusqu'à présent, tous nos composants étaient simples dans le sens où ils ne contenaient aucun état susceptible de changer au cours du cycle de vie du composant.

Ensuite, ajoutons un état au composant App de notre application à l'aide du state hook de React.

Nous allons modifier l'application comme suit. main.js revient à

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

et App.js change comme suit :

import { useState } from 'react'
const App = () => {
  const [ counter, setCounter ] = useState(0)
  setTimeout(    () => setCounter(counter + 1),    1000  )
  return (
    <div>{counter}</div>
  )
}

export default App

Dans la première ligne, le fichier importe la fonction useState :

import { useState } from 'react'

Le corps de la fonction qui définit le composant commence par l'appel de la fonction :

const [ counter, setCounter ] = useState(0)

L'appel de fonction ajoute state au composant et le rend initialisé avec la valeur zéro. La fonction renvoie un tableau qui contient deux éléments. Nous affectons les éléments aux variables counter et setCounter en utilisant la syntaxe d'affectation déstructurante présentée précédemment.

La variable counter reçoit la valeur initiale de state qui est zéro. La variable setCounter est affectée à une fonction qui servira à modifier l'état.

L'application appelle la fonction setTimeout et lui transmet deux paramètres : une fonction pour incrémenter l'état du compteur et un délai d'expiration d'une seconde:

setTimeout(
  () => setCounter(counter + 1),
  1000
)

La fonction passée en premier paramètre à la fonction setTimeout est invoquée une seconde après l'appel de la fonction setTimeout

() => setCounter(counter + 1)

Lorsque la fonction de modification d'état setCounter est appelée, React rend à nouveau le composant, ce qui signifie que le corps de la fonction du composant est réexécuté :

() => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  return (
    <div>{counter}</div>
  )
}

La deuxième fois que la fonction du composant est exécutée, elle appelle la fonction useState et renvoie la nouvelle valeur de l'état : 1. L'exécution à nouveau du corps de la fonction effectue également un nouvel appel de fonction à setTimeout, qui exécute le délai d'attente d'une seconde et incrémente à nouveau l'état counter . Étant donné que la valeur de la variable counter est 1, l'incrémentation de la valeur de 1 revient essentiellement à une expression définissant la valeur de counter sur 2.

() => setCounter(2)

Pendant ce temps, l'ancienne valeur de counter - "1" - est rendue à l'écran.

Chaque fois que le setCounter modifie l'état, il provoque le rendu du composant. La valeur de l'état sera incrémentée à nouveau après une seconde, et cela continuera à se répéter tant que l'application sera en cours d'exécution.

Si le composant ne s'affiche pas lorsque vous pensez qu'il le devrait, ou s'il s'affiche au "mauvais moment", vous pouvez déboguer l'application en enregistrant les valeurs des variables du composant dans la console. Si nous faisons les ajouts suivants à notre code :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  console.log('rendering...', counter)
  return (
    <div>{counter}</div>
  )
}

Il est facile de suivre les appels effectués à la fonction de rendu du composant App :

Capture d'écran de la fonction de rendu avec les outils de développement.x

Votre console de navigateur était-elle ouverte ? Si ce n'était pas le cas, promettez que ce sera la dernière fois qu'on vous le rappellera.

Gestion des événements

Nous avons déjà mentionné les gestionnaires d'événements qui sont enregistrés pour être appelés lorsque des événements spécifiques se produisent plusieurs fois dans la partie 0. Par exemple. l'interaction d'un utilisateur avec les différents éléments d'une page Web peut provoquer le déclenchement d'un ensemble de différents types d'événements.

Modifions l'application pour que l'augmentation du compteur se produise lorsqu'un utilisateur clique sur un bouton, ce qui est implémenté avec l'élément bouton.

Les éléments bouton prennent en charge les mouse events, dont click, l'événement le plus courant. L'événement clic sur un bouton peut également être déclenché avec le clavier ou un écran tactile malgré le nom mouse event.

Dans React, l'application d'un event handler à l'événement click se passe comme ceci :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const handleClick = () => {    console.log('clicked')  }
  return (
    <div>
      <div>{counter}</div>
      <button onClick={handleClick}>        plus      </button>    </div>
  )
}

Nous définissons la valeur de l'attribut onClick du bouton comme une référence à la fonction handleClick définie dans le code.

Désormais, chaque clic sur le bouton plus entraîne l'appel de la fonction handleClick, ce qui signifie que chaque événement de clic enregistrera un message cliqué dans la console du navigateur.

La fonction de gestionnaire d'événements peut également être définie directement dans l'affectation de valeur de l'attribut onClick :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => console.log('clicked')}>        plus
      </button>
    </div>
  )
}

En modifiant le gestionnaire d'événements sous la forme suivante

<button onClick={() => setCounter(counter + 1)}>
  plus
</button>

nous obtenons le comportement souhaité, ce qui signifie que la valeur de counter est augmentée de un et que le composant est re-rendu.

Ajoutons également un bouton pour réinitialiser le compteur :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>
        plus
      </button>
      <button onClick={() => setCounter(0)}>         zero      </button>    </div>
  )
}

Notre application est fin prête !

Le gestionnaire d'événements est une fonction

Nous définissons les gestionnaires d'événements pour nos boutons où nous déclarons leurs attributs onClick :

<button onClick={() => setCounter(counter + 1)}> 
  plus
</button>

Et si nous essayions de définir les gestionnaires d'événements sous une forme plus simple ?

<button onClick={setCounter(counter + 1)}> 
  plus
</button>

Cela casserait complètement notre application :

fullstack content

Que se passe-t-il? Un gestionnaire d'événements est censé être soit une fonction soit une référence de fonction, et lorsque nous écrivons :

<button onClick={setCounter(counter + 1)}>

le gestionnaire d'événements est en fait un appel de fonction. Dans de nombreuses situations, c'est ok, mais pas dans cette situation particulière. Au début, la valeur de la variable counter est 0. Lorsque React rend le composant pour la première fois, il exécute l'appel de fonction setCounter(0+1), et change la valeur de l'état du composant à 1. Cela entraînera un nouveau rendu du composant, React exécutera à nouveau l'appel de la fonction setCounter et l'état changera, conduisant à un autre rendu ...

Définissons les gestionnaires d'événements comme nous l'avons fait auparavant :

<button onClick={() => setCounter(counter + 1)}> 
  plus
</button>

Maintenant, l'attribut du bouton qui définit ce qui se passe lorsque le bouton est cliqué - onClick - a la valeur () => setCounter(counter + 1). La fonction setCounter est appelée uniquement lorsqu'un utilisateur clique sur le bouton.

Habituellement, définir des gestionnaires d'événements dans des modèles JSX n'est pas une bonne idée. Ici, ça va, car nos gestionnaires d'événements sont assez simples.

Séparons quand même les gestionnaires d'événements en fonctions distinctes :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)    const setToZero = () => setCounter(0)
  return (
    <div>
      <div>{counter}</div>
      <button onClick={increaseByOne}>        plus
      </button>
      <button onClick={setToZero}>        zero
      </button>
    </div>
  )
}

Ici, les gestionnaires d'événements ont été définis correctement. La valeur de l'attribut onClick est une variable contenant une référence à une fonction :

<button onClick={increaseByOne}> 
  plus
</button>

Transmission de l'état aux composants enfants

Il est recommandé d'écrire des composants React petits et réutilisables dans l'application et même dans les projets. Refactorisons notre application afin qu'elle soit composée de trois composants plus petits, un composant pour afficher le compteur et deux composants pour les boutons.

Commençons par implémenter un composant Display chargé d'afficher la valeur du compteur.

Une bonne pratique dans React consiste à remonter l'état dans la hiérarchie des composants. La documentation dit:

Souvent, plusieurs composants doivent refléter les mêmes données changeantes. Nous vous recommandons de remonter l'état partagé jusqu'à leur ancêtre commun le plus proche.

Plaçons donc l'état de l'application dans le composant App et transmettons-le au composant Display via props :

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

L'utilisation du composant est simple, car nous n'avons qu'à lui transmettre l'état du counter :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>      <button onClick={increaseByOne}>
        plus
      </button>
      <button onClick={setToZero}> 
        zero
      </button>
    </div>
  )
}

Tout fonctionne encore. Lorsque les boutons sont cliqués et que l'App est re-rendue, tous ses enfants, y compris le composant Display sont également re-rendus.

Ensuite, créons un composant Button pour les boutons de notre application. Nous devons passer le gestionnaire d'événements ainsi que le titre du bouton via les props du composant :

const Button = (props) => {
  return (
    <button onClick={props.onClick}>
      {props.text}
    </button>
  )
}

Notre composant App ressemble maintenant à ceci :

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const decreaseByOne = () => setCounter(counter - 1)  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>
      <Button        onClick={increaseByOne}        text='plus'      />      <Button        onClick={setToZero}        text='zero'      />           <Button        onClick={decreaseByOne}        text='minus'      />               </div>
  )
}

Étant donné que nous avons maintenant un composant Button facilement réutilisable, nous avons également implémenté une nouvelle fonctionnalité dans notre application en ajoutant un bouton qui peut être utilisé pour décrémenter le compteur.

Le gestionnaire d'événements est passé au composant Button via la prop handleClick. Le nom de la prop en lui-même n'est pas très significatif, mais notre choix de nom n'était pas complètement aléatoire.

Le tutoriel officiel de React suggère : "En React, il est conventionnel d'utiliser des noms de type onSomething pour les props qui représentent des événements et handleSomething pour les définitions de fonctions qui gèrent ces événements."

Les changements d'état entraînent un nouveau rendu

Revenons une fois de plus sur les grands principes de fonctionnement d'une application.

Lorsque l'application démarre, le code dans App est exécuté. Ce code utilise un hook useState pour créer l'état de l'application, en définissant une valeur initiale de la variable counter. Ce composant contient le composant Display - qui affiche la valeur du compteur, 0 - et trois composants Button. Les boutons ont tous des gestionnaires d'événements, qui sont utilisés pour changer l'état du compteur.

Lorsque l'un des boutons est cliqué, le gestionnaire d'événements est exécuté. Le gestionnaire d'événements modifie l'état du composant App avec la fonction setCounter.

L'appel d'une fonction qui modifie l'état provoque le rendu du composant.

Ainsi, si un utilisateur clique sur le bouton plus, le gestionnaire d'événements du bouton change la valeur de counter à 1, et le composant App est restitué. Cela entraîne également le rendu de ses sous-composants Display et Button. Display reçoit la nouvelle valeur du compteur, 1, comme prop. Les composants Button reçoivent des gestionnaires d'événements qui peuvent être utilisés pour modifier l'état du compteur.

Refactoring des composants

Le composant affichant la valeur du compteur est le suivant :

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

Le composant utilise uniquement le champ counter de ses props. Cela signifie que nous pouvons simplifier le composant en utilisant la destructuration, comme ceci :

const Display = ({ counter }) => {
  return (
    <div>{counter}</div>
  )
}

La fonction qui définit le composant ne contient que l'instruction de retour, nous pouvons donc définir la fonction en utilisant la forme plus compacte des fonctions fléchées :

const Display = ({ counter }) => <div>{counter}</div>

Nous pouvons également simplifier le composant Button.

const Button = (props) => {
  return (
    <button onClick={props.onClick}>
      {props.text}
    </button>
  )
}

Nous pouvons utiliser la destructuration pour obtenir uniquement les champs requis des props et utiliser la forme plus compacte des fonctions fléchées :

NB : Lors de la création de vos propres composants, vous pouvez nommer les props des gestionnaires d'événements comme bon vous semble, pour cela, vous pouvez vous référer à la documentation de React sur Naming event handler props. Cela se présente comme suit :

Par convention, les props des gestionnaires d'événements doivent commencer par on, suivi d'une lettre majuscule. Par exemple, la prop onClick du composant Button aurait pu s'appeler onSmash :

const Button = ({ onClick, text }) => (
  <button onClick={onClick}>
    {text}
  </button>
)

ou encore comme suit :

const Button = ({ onSmash, text }) => (
  <button onClick={onSmash}>
    {text}
  </button>

Nous pouvons simplifier une fois de plus le composant Button en déclarant l'instruction de retour en une seule ligne :

const Button = ({ onSmash, text }) => <button onClick={onSmash}>{text}</button>

NB : Cependant, veillez à ne pas trop simplifier vos composants, car cela rend l'ajout de complexité plus fastidieux par la suite.