Aller au contenu

b

props.children et proptypes

Afficher le formulaire de connexion uniquement lorsque c'est approprié

Modifions l'application pour que le formulaire de connexion ne soit pas affiché par défaut:

navigateur montrant le bouton de connexion par défaut

Le formulaire de connexion apparaît lorsque l'utilisateur appuie sur le bouton login:

utilisateur sur l'écran de connexion sur le point d'appuyer sur annuler

L'utilisateur peut fermer le formulaire de connexion en cliquant sur le bouton cancel.

Commençons par extraire le formulaire de connexion dans son propre composant:

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
  return (
    <div>
      <h2>Login</h2>

      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
      </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

export default LoginForm

L'état et toutes les fonctions qui s'y rapportent sont définis à l'extérieur du composant et sont passés au composant sous forme de props.

Remarquez que les props sont assignées à des variables par le biais de la décomposition, ce qui signifie qu'au lieu d'écrire:

const LoginForm = (props) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={props.handleSubmit}>
        <div>
          username
          <input
            value={props.username}
            onChange={props.handleChange}
            name="username"
          />
        </div>
        // ...
        <button type="submit">login</button>
      </form>
    </div>
  )
}

où les propriétés de l'objet props sont accessibles par exemple via props.handleSubmit, les propriétés sont directement assignées à leurs propres variables.

Une manière rapide de mettre en oeuvre la fonctionnalité consiste à modifier la fonction loginForm du composant App de cette façon:

const App = () => {
  const [loginVisible, setLoginVisible] = useState(false)
  // ...

  const loginForm = () => {
    const hideWhenVisible = { display: loginVisible ? 'none' : '' }
    const showWhenVisible = { display: loginVisible ? '' : 'none' }

    return (
      <div>
        <div style={hideWhenVisible}>
          <button onClick={() => setLoginVisible(true)}>log in</button>
        </div>
        <div style={showWhenVisible}>
          <LoginForm
            username={username}
            password={password}
            handleUsernameChange={({ target }) => setUsername(target.value)}
            handlePasswordChange={({ target }) => setPassword(target.value)}
            handleSubmit={handleLogin}
          />
          <button onClick={() => setLoginVisible(false)}>cancel</button>
        </div>
      </div>
    )
  }

  // ...
}

L'état du composant App contient maintenant le booléen loginVisible, qui définit si le formulaire de connexion doit être montré à l'utilisateur ou non.

La valeur de loginVisible est basculée avec deux boutons. Les deux boutons ont leurs gestionnaires d'événements définis directement dans le composant:

<button onClick={() => setLoginVisible(true)}>log in</button>

<button onClick={() => setLoginVisible(false)}>cancel</button>

La visibilité du composant est définie en donnant au composant une règle de style en ligne, où la valeur de la propriété display est none si nous ne voulons pas que le composant soit affiché:

const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }

<div style={hideWhenVisible}>
  // button
</div>

<div style={showWhenVisible}>
  // button
</div>

Nous utilisons une fois de plus l'opérateur ternaire "point d'interrogation". Si loginVisible est true, alors la règle CSS du composant sera:

display: 'none';

Si loginVisible est false, alors display ne recevra aucune valeur liée à la visibilité du composant.

Les enfants des composants, alias props.children

Le code lié à la gestion de la visibilité du formulaire de connexion pourrait être considéré comme sa propre entité logique, et pour cette raison, il serait bon de l'extraire du composant App pour le placer dans un composant séparé.

Notre objectif est de mettre en oeuvre un nouveau composant Togglable qui peut être utilisé de la manière suivante:

<Togglable buttonLabel='login'>
  <LoginForm
    username={username}
    password={password}
    handleUsernameChange={({ target }) => setUsername(target.value)}
    handlePasswordChange={({ target }) => setPassword(target.value)}
    handleSubmit={handleLogin}
  />
</Togglable>

La manière dont le composant est utilisé est légèrement différente de nos composants précédents. Le composant a des balises d'ouverture et de fermeture qui entourent un composant LoginForm. Dans la terminologie React, LoginForm est un composant enfant de Togglable.

Nous pouvons ajouter tous les éléments React que nous voulons entre les balises d'ouverture et de fermeture de Togglable, comme ceci par exemple:

<Togglable buttonLabel="reveal">
  <p>this line is at start hidden</p>
  <p>also this is hidden</p>
</Togglable>

Le code pour le composant Togglable est montré ci-dessous:

import { useState } from 'react'

const Togglable = (props) => {
  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
}

export default Togglable

La partie nouvelle et intéressante du code est props.children, qui est utilisée pour référencer les composants enfants du composant. Les composants enfants sont les éléments React que nous définissons entre les balises d'ouverture et de fermeture d'un composant.

Cette fois, les enfants sont rendus dans le code utilisé pour le rendu du composant lui-même:

<div style={showWhenVisible}>
  {props.children}
  <button onClick={toggleVisibility}>cancel</button>
</div>

Contrairement aux props "normales" que nous avons vues précédemment, children est automatiquement ajouté par React et existe toujours. Si un composant est défini avec une balise de fermeture automatique />, comme ceci:

<Note
  key={note.id}
  note={note}
  toggleImportance={() => toggleImportanceOf(note.id)}
/>

Alors, props.children est un tableau vide.

Le composant Togglable est réutilisable et nous pouvons l'utiliser pour ajouter une fonctionnalité de basculement de visibilité similaire au formulaire utilisé pour créer de nouvelles notes.

Avant cela, extrayons le formulaire de création de notes dans un composant:

const NoteForm = ({ onSubmit, handleChange, value}) => {
  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={onSubmit}>
        <input
          value={value}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

Ensuite, définissons le composant de formulaire à l'intérieur d'un composant Togglable:

<Togglable buttonLabel="new note">
  <NoteForm
    onSubmit={addNote}
    value={newNote}
    handleChange={handleNoteChange}
  />
</Togglable>

Vous pouvez trouver le code de notre application actuelle dans son intégralité dans la branche part5-4 de ce dépôt GitHub.

État des formulaires

L'état de l'application est actuellement dans le composant App.

La documentation de React dit ce qui suit sur l'endroit où placer l'état:

Parfois, vous voulez que l'état de deux composants change toujours ensemble. Pour ce faire, retirez l'état de ces deux composants, déplacez-le vers leur parent commun le plus proche, puis transmettez-le à ces composants via les props. Cela est connu sous le nom de remontée d'état, et c’est l’une des choses les plus courantes que vous ferez en écrivant du code React.

Si nous réfléchissons à l'état des formulaires, donc par exemple au contenu d'une nouvelle note avant qu'elle n'ait été créée, le composant App n'en a besoin pour rien. Nous pourrions tout aussi bien déplacer l'état des formulaires vers les composants correspondants.

Le composant pour une note change comme suit:

import { useState } from 'react'

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('')

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: true
    })

    setNewNote('')
  }

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={event => setNewNote(event.target.value)}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

NOTE En même temps, nous avons changé le comportement de l'application de sorte que les nouvelles notes soient importantes par défaut, c'est-à-dire que le champ important reçoit la valeur true.

L'attribut d'état newNote et le gestionnaire d'événements responsable de sa modification ont été déplacés du composant App au composant responsable du formulaire de note.

Il ne reste qu'une seule prop, la fonction createNote, que le formulaire appelle lorsqu'une nouvelle note est créée.

Le composant App devient plus simple maintenant que nous nous sommes débarrassés de l'état newNote et de son gestionnaire d'événements. La fonction addNote pour créer de nouvelles notes reçoit une nouvelle note en paramètre, et la fonction est la seule prop que nous envoyons au formulaire:

const App = () => {
  // ...
  const addNote = (noteObject) => {    noteService
      .create(noteObject)
      .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
  const noteForm = () => (
    <Togglable buttonLabel='new note'>
      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

Nous pourrions faire de même pour le formulaire de connexion, mais nous laisserons cela pour un exercice optionnel.

Le code de l'application peut être trouvé sur GitHub, branche part5-5.

Références aux composants avec ref

Notre mise en oeuvre actuelle est assez bonne; il y a un aspect qui pourrait être amélioré.

Après la création d'une nouvelle note, il serait logique de masquer le formulaire de la nouvelle note. Actuellement, le formulaire reste visible. Il y a un léger problème pour masquer le formulaire. La visibilité est contrôlée avec la variable visible à l'intérieur du composant Togglable. Comment pouvons-nous y accéder de l'extérieur du composant ?

Il existe de nombreuses manières d'implémenter la fermeture du formulaire depuis le composant parent, mais utilisons le mécanisme de ref de React, qui offre une référence au composant.

Faisons les changements suivants au composant App:

import { useState, useEffect, useRef } from 'react'
const App = () => {
  // ...
  const noteFormRef = useRef()
  const noteForm = () => (
    <Togglable buttonLabel='new note' ref={noteFormRef}>      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

Le hook useRef est utilisé pour créer une référence noteFormRef, qui est attribuée au composant Togglable contenant le formulaire de création de note. La variable noteFormRef agit comme une référence au composant. Ce hook garantit que la même référence (ref) est conservée tout au long des rendus du composant.

Nous apportons également les changements suivants au composant Togglable:

import { useState, forwardRef, useImperativeHandle } from 'react'
const Togglable = forwardRef((props, refs) => {  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  useImperativeHandle(refs, () => {    return {      toggleVisibility    }  })
  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})
export default Togglable

La fonction qui crée le composant est encapsulée à l'intérieur d'un appel de fonction forwardRef. De cette manière, le composant peut accéder à la référence qui lui est attribuée.

Le composant utilise le hook useImperativeHandle pour rendre sa fonction toggleVisibility disponible à l'extérieur du composant.

Nous pouvons maintenant masquer le formulaire en appelant noteFormRef.current.toggleVisibility() après qu'une nouvelle note a été créée:

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteFormRef.current.toggleVisibility()    noteService
      .create(noteObject)
      .then(returnedNote => {     
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
}

Pour résumer, la fonction useImperativeHandle est un hook de React, qui est utilisé pour définir des fonctions dans un composant, qui peuvent être invoquées de l'extérieur du composant.

Cette astuce fonctionne pour changer l'état d'un composant, mais elle semble un peu désagréable. Nous aurions pu accomplir la même fonctionnalité avec un code légèrement plus propre en utilisant les composants basés sur les classes du "vieux React". Nous examinerons ces composants de classe pendant la partie 7 du matériel du cours. Jusqu'à présent, c'est la seule situation où l'utilisation des hooks de React mène à un code qui n'est pas plus propre qu'avec les composants de classe.

Il existe également d'autres cas d'utilisation pour les refs que l'accès aux composants React.

Vous pouvez trouver le code de notre application actuelle dans son intégralité dans la branche part5-6 de ce dépôt GitHub.

Un point sur les composants

Lorsque nous définissons un composant en React:

const Togglable = () => ...
  // ...
}

And use it like this:

<div>
  <Togglable buttonLabel="1" ref={togglable1}>
    first
  </Togglable>

  <Togglable buttonLabel="2" ref={togglable2}>
    second
  </Togglable>

  <Togglable buttonLabel="3" ref={togglable3}>
    third
  </Togglable>
</div>

Nous créons trois instances distinctes du composant qui ont toutes leur état séparé:

navigateur de trois composants togglable

L'attribut ref est utilisé pour assigner une référence à chacun des composants dans les variables togglable1, togglable2 et togglable3.

Le serment du développeur full stack mis à jour

Le nombre de parties mobiles augmente. En même temps, la probabilité de se retrouver dans une situation où nous cherchons un bug au mauvais endroit augmente. Nous devons donc être encore plus systématiques.

Nous devrions donc étendre une fois de plus notre serment:

Le développement full stack est extrêmement difficile, c'est pourquoi j'utiliserai tous les moyens possibles pour le rendre plus facile

  • J'aurai ma console de développeur de navigateur ouverte tout le temps
  • J'utiliserai l'onglet réseau des outils de développement du navigateur pour m'assurer que le frontend et le backend communiquent comme je le souhaite
  • Je garderai constamment un oeil sur l'état du serveur pour m'assurer que les données envoyées par le frontend y sont sauvegardées comme je le souhaite
  • Je garderai un oeil sur la base de données: le backend y sauvegarde-t-il les données dans le bon format
  • Je progresse par petites étapes
  • lorsque je suspecte qu'il y a un bug dans le frontend, je m'assure que le backend fonctionne à coup sûr
  • lorsque je suspecte qu'il y a un bug dans le backend, je m'assure que le frontend fonctionne à coup sûr
  • J'écrirai beaucoup de console.log pour m'assurer que je comprends comment le code et les tests se comportent et pour aider à localiser les problèmes
  • Si mon code ne fonctionne pas, je n'écrirai pas plus de code. Au lieu de cela, je commence à supprimer le code jusqu'à ce qu'il fonctionne ou je reviens à un état où tout fonctionnait encore
  • Si un test ne passe pas, je m'assure que la fonctionnalité testée fonctionne à coup sûr dans l'application
  • Lorsque je demande de l'aide sur le canal Discord du cours ou ailleurs, je formule correctement mes questions, voir ici comment demander de l'aide

PropTypes

Le composant Togglable suppose qu'on lui donne le texte pour le bouton via la prop buttonLabel. Si nous oublions de le définir pour le composant:

<Togglable> buttonLabel forgotten... </Togglable>

L'application fonctionne, mais le navigateur affiche un bouton qui n'a pas de texte d'étiquette.

Nous aimerions imposer que lorsque le composant Togglable est utilisé, la prop de texte d'étiquette du bouton doit se voir attribuer une valeur.

Les props attendues et requises d'un composant peuvent être définies avec le package prop-types. Installons le package:

npm install prop-types

Nous pouvons définir la prop buttonLabel comme une prop obligatoire ou required de type chaîne de caractères comme montré ci-dessous:

import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ..
})

Togglable.propTypes = {
  buttonLabel: PropTypes.string.isRequired
}

La console affichera le message d'erreur suivant si la prop est laissée indéfinie:

erreur de console indiquant que buttonLabel est indéfini

L'application fonctionne toujours et rien ne nous oblige à définir des props malgré les définitions de PropTypes. Cela dit, il est extrêmement peu professionnel de laisser n'importe quel message d'erreur en rouge dans la console du navigateur.

Définissons également les PropTypes pour le composant LoginForm:

import PropTypes from 'prop-types'

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
    // ...
  }

LoginForm.propTypes = {
  handleSubmit: PropTypes.func.isRequired,
  handleUsernameChange: PropTypes.func.isRequired,
  handlePasswordChange: PropTypes.func.isRequired,
  username: PropTypes.string.isRequired,
  password: PropTypes.string.isRequired
}

Si le type d'une prop passée est incorrect, par exemple, si nous essayons de définir la prop handleSubmit comme une chaîne de caractères, cela entraînera l'avertissement suivant:

erreur de console disant que handleSubmit attendait une fonction

ESlint

Dans la partie 3, nous avons configuré l'outil de style de code ESlint pour le backend. Prenons ESlint en main pour l'utiliser également dans le frontend.

Vite a installé ESlint dans le projet par défaut, il ne nous reste donc plus qu'à définir notre configuration souhaitée dans le fichier .eslintrc.cjs.

Ensuite, nous commencerons à tester le frontend et afin d'éviter des erreurs de linter indésirables et non pertinentes, nous installerons le package eslint-plugin-jest:

npm install --save-dev eslint-plugin-jest

Créons un fichier .eslintrc.cjs avec le contenu suivant:

module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
    "jest/globals": true
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: '18.2' } },
  plugins: ['react-refresh', 'jest'],
  rules: {
    "indent": [
        "error",
        2  
    ],
    "linebreak-style": [
        "error",
        "unix"
    ],
    "quotes": [
        "error",
        "single"
    ],
    "semi": [
        "error",
        "never"
    ],
    "eqeqeq": "error",
    "no-trailing-spaces": "error",
    "object-curly-spacing": [
        "error", "always"
    ],
    "arrow-spacing": [
        "error", { "before": true, "after": true }
    ],
    "no-console": 0,
    "react/react-in-jsx-scope": "off",
    "react/prop-types": 0,
    "no-unused-vars": 0    
  },
}

NOTE: Si vous utilisez Visual Studio Code avec le plugin ESLint, vous pourriez avoir besoin d'ajouter un paramètre de workspace pour qu'il fonctionne. Si vous voyez l'erreur Failed to load plugin react: Cannot find module 'eslint-plugin-react', une configuration supplémentaire est nécessaire. Ajouter la ligne "eslint.workingDirectories": [{ "mode": "auto" }] au fichier settings.json dans l'espace de travail semble fonctionner. Voir ici pour plus d'informations.

Créons un fichier .eslintignore avec le contenu suivant à la racine du dépôt:

node_modules
dist
.eslintrc.cjs

Maintenant, les répertoires dist et node_modules seront ignorés lors du linting.

Comme d'habitude, vous pouvez effectuer le linting soit depuis la ligne de commande avec la commande

npm run Lint

ou en utilisant le plugin Eslint de l'éditeur.

Le composant Togglable provoque un avertissement désagréable La définition du composant manque d'un nom d'affichage:

vscode montrant une erreur de définition de composant

Les react-devtools révèlent également que le composant n'a pas de nom:

1react devtools montrant forwardRef comme anonyme

Heureusement, cela est facile à corriger

import { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ...
})

Togglable.displayName = 'Togglable'
export default Togglable

Vous pouvez trouver le code de notre application actuelle dans son intégralité dans la branche part5-7 de ce dépôt GitHub.