logo
    • Accueil
    • Catégories
    • A propos
  • fr-languageFrançais
Interface utilisateurPar Pierre Colart

React: Composants non controlés et controlés

Introduction

Dans cet article, nous allons apprendre à implémenter des formulaires avec React. Lorsque nous construisons une application réelle avec React, il est important d'interagir avec les utilisateurs. Les formulaires sont la solution la plus courante pour demander des informations à nos utilisateurs dans le navigateur. Cependant, en raison du fonctionnement déclaratif de la bibliothèque, la manipulation des champs de saisie et autres éléments de formulaire n'est pas toujours évidente. Nous allons donc apprendre à utiliser des composants non contrôlés et contrôlés pour traiter les formulaires dans les sections suivantes.

Composants non controlés

Les composants non contrôlés sont similaires aux entrées de formulaire HTML standard pour lesquelles vous ne pouvez pas gérer directement la valeur. Au lieu de cela, la valeur est gérée par le DOM, mais vous pouvez accéder à cette valeur en utilisant une référence React.

 import { useState, ChangeEvent, MouseEvent } from 'react'

const Uncontrolled = () => {
  const [value, setValue] = useState('')

  return (
    <form> 
      <input type="text" /> 
      <button>Submit</button> 
    </form>
  ) 
}

export default Uncontrolled

Si nous exécutons l'extrait précédent dans le navigateur, nous verrons un champ de saisie où nous pouvons taper quelque chose ainsi qu'un bouton cliquable. Cela est dû à l'utilisation d'un composant non contrôlé, où nous ne définissons pas la valeur du champ d'entrée, mais laissons le composant gérer son propre état interne. Toutefois, nous souhaitons probablement faire quelque chose avec la valeur de l'élément lorsque le bouton Soumettre est cliqué. Par exemple, nous pourrions vouloir envoyer les données à une API.

Nous pouvons facilement le faire en ajoutant un écouteur onChange.

 const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value)
}

Lorsqu'un événement est capturé par l'écouteur d'événement, il est accompagné d'un objet contenant des informations sur l'événement. Nous nous concentrons sur la valeur du champ qui a généré cet événement, qui est représenté par la cible de l'événement. Pour commencer, nous enregistrons simplement cette valeur, car il est important de procéder étape par étape. Toutefois, nous prévoyons bientôt de stocker cette valeur dans l'état.

 return (
  <form> 
    <input type="text" onChange={handleChange} /> 
    <button>Submit</button> 
  </form> 
)

Si nous publions le composant dans le navigateur et saisissons le mot "React" dans le champ de formulaire, nous pourrons voir quelque chose de similaire à ce qui suit dans la console :

 R
Re
Rea
Reac
React

Chaque fois que la valeur de l'entrée change, l'écouteur est déclenché. Ainsi, notre fonction est appelée une fois pour chaque caractère saisi par l'utilisateur. La prochaine étape consiste à stocker la valeur saisie par l'utilisateur et à la rendre disponible pour l'utilisateur lorsqu'il clique sur le bouton "Soumettre".

 const handleChange = (e: ChangeEvent<HTMLInputElement>) => { 
  setValue(e.target.value)
}

La détection de la soumission du formulaire est très similaire à l'écoute de l'événement de modification du champ de saisie. Ce sont tous deux des événements qui sont déclenchés par le navigateur lorsqu'une action est effectuée. Nous allons maintenant définir une fonction, où nous allons simplement enregistrer la valeur saisie. Dans un scénario réel, vous pouvez envoyer les données à un point de terminaison d'API ou les transmettre à un autre composant. Cette fonction est appelée handleSubmit.

 const handleSubmit = (e: MouseEvent<HTMLButtonElement>) => { 
  e.preventDefault()
   
  console.log(value)
}

Ce gestionnaire est assez simple ; nous enregistrons simplement la valeur actuellement stockée dans l'état. Nous voulons également remplacer le comportement par défaut du navigateur lors de la soumission du formulaire, afin d'effectuer une action personnalisée. Cette approche semble raisonnable et fonctionne bien pour un seul champ. Cependant, que se passe-t-il si nous avons plusieurs champs ? Imaginons que nous ayons des dizaines de champs différents ?

Commençons par un exemple de base, où nous créons manuellement chaque champ et gestionnaire, puis voyons comment nous pouvons améliorer cette approche en appliquant différents niveaux d'optimisation. Nous allons créer un nouveau formulaire avec des champs de prénom et de nom :

 const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')

Nous initialisons les deux champs à l'intérieur de l'état et nous définissons également un gestionnaire d'événements pour chacun des champs. Cependant, cette approche ne fonctionne pas très bien lorsqu'il y a beaucoup de champs. Il est donc important de bien comprendre le problème avant de passer à une solution plus flexible. Maintenant, nous allons implémenter les nouveaux gestionnaires :

 const handleChangeFirstName = ({ target: { value } }) => {
  setFirstName(value) 
} 
   
const handleChangeLastName = ({ target: { value } }) => {
  setLastName(value) 
}

Nous devons également apporter quelques modifications au gestionnaire de soumission pour qu'il affiche le prénom et le nom de famille lorsqu'il est cliqué :

 const handleSubmit = (e: MouseEvent<HTMLButtonElement>) => { 
  e.preventDefault()
   
  console.log(`${firstName} ${lastName}`)
}

Enfin :

 return ( 
  <form onSubmit={handleSubmit}> 
    <input type="text" onChange={handleChangeFirstName} /> 
    <input type="text" onChange={handleChangeLastName} /> 
    <button>Submit</button> 
  </form> 
)

Nous sommes prêts : si nous exécutons le composant précédent dans le navigateur, nous verrons deux champs. Si nous tapons dans le premier et dans le second, nous verrons le nom complet affiché dans la console du navigateur lorsque le formulaire est soumis.

Encore une fois, cela fonctionne bien et nous pouvons faire des choses intéressantes de cette façon. Cependant, cela devient difficile à gérer lorsque nous avons des scénarios complexes, sans nous obliger à écrire beaucoup de code passe-partout. Voyons comment nous pouvons optimiser cela un peu. Notre objectif est d'utiliser un seul gestionnaire de modifications afin de pouvoir ajouter un nombre arbitraire de champs sans créer de nouveaux écouteurs. Revenons au composant et modifions notre état :

 const [values, setValues] = useState({ firstName: '', lastName: '' })

Il est toujours possible que nous voulions initialiser les valeurs, et plus loin dans cette section, nous verrons comment fournir des valeurs préremplies pour le formulaire. Cependant, la partie intéressante est de savoir comment nous pouvons modifier l'implémentation du gestionnaire pour le rendre flexible et applicable à différents domaines :

 const handleChange = ({ target: { name, value } }) => {    
  setValues({ 
    ...values,
    [name]: value
  })
}

Comme nous l'avons vu précédemment, la propriété target de l'événement que nous recevons représente le champ d'entrée qui a déclenché l'événement. Nous pouvons donc utiliser le nom du champ et sa valeur comme variables. Nous devons ensuite définir le nom de chaque champ :

 return ( 
  <form onSubmit={handleSubmit}> 
    <input 
      type="text" 
      name="firstName" 
      onChange={handleChange} 
    /> 
    <input 
      type="text" 
      name="lastName" 
      onChange={handleChange} 
    /> 
    <button>Submit</button> 
  </form> 
)

Exactement ! Nous pouvons maintenant ajouter autant de champs que nous le souhaitons sans avoir à créer de nouveaux gestionnaires. Cela rend notre formulaire beaucoup plus flexible et facile à gérer.

Composants controlés

Un composant contrôlé est un composant React qui contrôle les valeurs des éléments d'entrée dans un formulaire en utilisant l'état du composant. Dans cette section, nous allons explorer comment nous pouvons pré-remplir les champs du formulaire avec des valeurs spécifiques que nous pouvons recevoir du serveur ou des props du parent. Pour bien comprendre ce concept, nous allons partir d'un composant de fonction simple et y ajouter des fonctionnalités au fur et à mesure.

 const Controlled = () => ( 
  <form> 
    <input type="text" value="Hello React" /> 
    <button>Submit</button> 
  </form> 
)

Si nous exécutons ce composant dans le navigateur, nous remarquons qu'il affiche la valeur par défaut comme prévu, mais ne nous permet pas de modifier la valeur ou de saisir autre chose à l'intérieur. La raison en est que dans React, nous déclarons ce que nous voulons voir à l'écran, et la définition d'un attribut à valeur fixe entraîne toujours le rendu de cette valeur, quelles que soient les autres actions entreprises. Ce n'est probablement pas un comportement que nous souhaitons dans une application du monde réel.

Si nous ouvrons la console, nous obtenons le message d'erreur suivant : "You provided a value prop to a form field without an onChange handler. This will render a read-only field." React nous indique que nous faisons quelque chose de mal.

Si nous souhaitons simplement que le champ de saisie ait une valeur par défaut et que nous puissions la modifier en tapant, nous pouvons utiliser la propriété defaultValue.

 import { useState } from 'react'

const Controlled = () => {
  return (
    <form> 
      <input type="text" defaultValue="Hello React" /> 
      <button>Submit</button> 
    </form> 
  )
}

export default Controlled

De cette manière, le champ s'affichera avec la valeur par défaut lorsqu'il sera rendu, mais l'utilisateur pourra ensuite saisir n'importe quoi à l'intérieur et modifier sa valeur. Maintenant, ajoutons quelques états :

 const [values, setValues] = useState({ firstName: 'Carlos', lastName: 'Santana' })

Les gestionnaires restent les mêmes que les précédents :

 const handleChange = ({ target: { name, value } }) => { 
  setValues({ 
    [name]: value 
  })
} 
   
const handleSubmit = (e) => { 
  e.preventDefault()
   
  console.log(`${values.firstName} ${values.lastName}`)
}

En fait, nous allons utiliser les valeurs des champs de saisie pour définir leurs valeurs initiales, ainsi que celles mises à jour :

 return ( 
  <form onSubmit={handleSubmit}> 
    <input 
      type="text" 
      name="firstName" 
      value={values.firstName} 
      onChange={handleChange} 
    /> 
    <input 
      type="text" 
      name="lastName" 
      value={values.lastName} 
      onChange={handleChange} 
    /> 
    <button>Submit</button> 
  </form> 
)

La première fois que le formulaire est rendu, React utilise les valeurs initiales de l'état comme valeurs des champs d'entrée. Lorsque l'utilisateur tape quelque chose dans le champ, la fonction handleChange est appelée et la nouvelle valeur du champ est stockée dans l'état.

Lorsque l'état change, React re-rend le composant et utilise à nouveau les nouvelles valeurs des champs d'entrée. Nous avons maintenant un contrôle total sur les valeurs des champs, et nous appelons ce modèle des composants contrôlés.

Gestion des événements

Les événements fonctionnent légèrement différemment selon les navigateurs. React essaie d'abstraire le fonctionnement des événements et de donner aux développeurs une interface cohérente pour gérer les événements. C'est une fonctionnalité intéressante de React car nous pouvons oublier les navigateurs ciblés et écrire des gestionnaires d'événements et des fonctions indépendantes du fournisseur.

Pour proposer cette fonctionnalité, React a introduit le concept d'événement synthétique. Un événement synthétique est un objet qui encapsule l'objet événement d'origine fourni par le navigateur, et il a les mêmes propriétés, quel que soit l'endroit où il est créé. Pour attacher un écouteur d'événement à un nœud et obtenir l'objet événement lorsque l'événement est déclenché, nous pouvons utiliser une convention simple qui rappelle la façon dont les événements sont attachés aux nœuds DOM. En fait, nous pouvons utiliser le nom de l'événement camelCase (par exemple, ) pour définir le callback à déclencher lorsque les événements se produisent. Une convention courante consiste à nommer les fonctions du gestionnaire d'événements d'après le nom de l'événement et à les préfixer en utilisant (par exemple, ).

Nous avons vu ce modèle en action dans les exemples précédents, où nous écoutions l'événement des champs de formulaire. Reprenons un exemple d'écouteur d'événement de base pour voir comment nous pouvons organiser plusieurs événements dans le même composant de manière plus agréable. Nous allons implémenter un simple bouton, et commencer comme d'habitude, en créant un composant :

 const Button = () => {
}
export default Button

Ensuite, nous définissons le gestionnaire d'événements :

 const handleClick = (syntheticEvent) => { 
  console.log(syntheticEvent instanceof MouseEvent)
  console.log(syntheticEvent.nativeEvent instanceof MouseEvent)
}

Comme vous pouvez le voir ici, nous faisons une chose très simple : nous vérifions simplement le type d'objet d'événement que nous recevons de React et le type d'événement natif qui lui est attaché. Nous nous attendons à ce que le premier revienne false et le second revienne true. Vous ne devriez jamais avoir besoin d'accéder à l'événement natif d'origine, mais il est bon de savoir que vous pouvez le faire si vous en avez besoin. Enfin, nous définissons le bouton avec l'attribut onClick auquel nous attachons notre gestionnaire d'événements :

 return ( 
  <button onClick={handleClick}>Click me!</button> 
)

Supposons maintenant que nous voulions attacher un deuxième gestionnaire au bouton qui écoute l'événement de double-clic. Une solution consiste à créer un nouveau gestionnaire séparé et à l'attacher au bouton en utilisant l'attribut onDoubleClick, comme suit :

 <button 
  onClick={handleClick} 
  onDoubleClick={handleDoubleClick} 
> 
  Click me! 
</button>

⚠️ N'oublions pas que notre objectif est toujours d'écrire moins de code passe-partout et d'éviter la duplication de code. Pour cette raison, une pratique courante consiste à écrire un seul gestionnaire d'événements pour chaque composant, qui peut déclencher différentes actions en fonction du type d'événement.

 const handleEvent = (event) => { 
  switch (event.type) { 
    case 'click': 
      console.log('clicked')
      break
   
    case 'dblclick': 
      console.log('double clicked')
      break
   
    default: 
      console.log('unhandled', event.type)
  } 
}

Le gestionnaire d'événements générique reçoit l'objet événement et active le type d'événement pour déclencher l'action appropriée. Cela est particulièrement utile si nous voulons appeler une fonction sur chaque événement (par exemple, pour les analyses) ou si certains événements partagent la même logique. Enfin, nous attachons le nouvel écouteur d'événement à l'attribut onClick et onDoubleClick respectivement :

 return ( 
  <button 
    onClick={handleEvent} 
    onDoubleClick={handleEvent} 
  > 
    Click me! 
  </button> 
) 

Désormais, lorsque nous avons besoin de créer un nouveau gestionnaire d'événements pour le même composant, au lieu de créer une nouvelle méthode et de la lier, nous pouvons simplement ajouter un nouveau cas au commutateur. Il est important de noter que les événements synthétiques dans React sont réutilisés et qu'il existe un seul gestionnaire global. Cela signifie que nous ne pouvons pas stocker un événement synthétique et le réutiliser ultérieurement car il devient nul juste après l'action. Bien que cela soit très efficace en termes de performances, cela peut être problématique si nous voulons stocker l'événement dans l'état du composant pour une raison quelconque. Pour résoudre ce problème, React nous fournit une méthode sur les événements synthétiques, que nous pouvons appeler pour rendre l'événement persistant afin que nous puissions le stocker et le récupérer plus tard (persist).

Le deuxième détail d'implémentation intéressant concerne à nouveau les performances et concerne la manière dont React attache les gestionnaires d'événements au DOM. Chaque fois que nous utilisons l'attribut on, nous décrivons à React le comportement que nous voulons obtenir, mais la bibliothèque n'attache pas le gestionnaire d'événements réel aux nœuds DOM sous-jacents. Au lieu de cela, il attache un seul gestionnaire d'événements à l'élément racine, qui écoute tous les événements, grâce au event bubbling. Lorsqu'un événement qui nous intéresse est déclenché par le navigateur, React appelle le gestionnaire sur les composants spécifiques en son nom. Cette technique est appelée délégation d'événements et est utilisée pour optimiser la mémoire et la vitesse.

Explorer les références

React est apprécié pour sa nature déclarative, qui permet de décrire simplement ce qui doit être affiché à l'écran et de laisser React gérer les communications avec le navigateur. Cependant, dans certains cas, il peut être nécessaire d'accéder aux nœuds DOM sous-jacents pour effectuer certaines opérations impératives. Bien que cela devrait être évité dans la plupart des cas, il est important de savoir comment cela fonctionne afin de prendre la bonne décision.

Supposons que nous voulons créer un formulaire simple avec un champ d'entrée et un bouton, et que nous voulons que le champ d'entrée soit ciblé lorsque le bouton est cliqué. Pour ce faire, nous devons appeler la méthode sur le nœud d'entrée, qui est l'instance DOM réelle, dans la fenêtre du navigateur. Nous allons donc créer un composant appelé "Focus".

 import { useRef } from 'react'

const Focus = () => {
  const inputRef = useRef(null)
}

export default Focus

Ensuite, nous implémentons la méthode de clique :

 const handleClick = () => { 
  inputRef.current.focus()
} 

Comme vous pouvez le voir, nous référençons l'attribut current de inputRef et appelons la méthode focus dessus :

 return ( 
  <> 
    <input 
      type="text" 
      ref={inputRef} 
    /> 
    <button onClick={handleClick}>Set Focus</button> 
  </> 
)

Le cœur de la logique de ce composant est la définition d'une fonction sur l'attribut ref de l'élément d'entrée à l'intérieur du formulaire. Cette fonction est appelée lorsque le composant est monté, et le paramètre element représente l'instance DOM de l'entrée. Il est important de noter que lorsque le composant est démonté, le même callback est appelé avec un paramètre null pour libérer la mémoire.

Dans ce callback, nous stockons la référence de l'élément pour pouvoir l'utiliser ultérieurement. Ensuite, nous avons le bouton avec son gestionnaire d'événements. Si nous exécutons ce code dans un navigateur, le formulaire avec le champ et le bouton s'affichera, et en cliquant sur le bouton, le champ de saisie sera ciblé, comme nous le souhaitions.

Il est important de noter que l'utilisation de références doit être évitée autant que possible, car elle force le code à être plus impératif et peut rendre le code plus difficile à lire et à maintenir.

Pierre Colart

Developpeur et architecte passionné, qui souhaite partagé son univers et ses découvertes afin de rendre les choses plus simple pour chacun

Voir le profil

Les derniers articles

Sequences, Time Series et Prediction

© 2023 Switch case. Made with by Pierre Colart