e
Styliser vos applications React
L'apparence de notre application actuelle est assez modeste. Dans l'exercice 0.2, le travail consistait à suivre le tutoriel CSS de Mozilla.
Avant de passer à la partie suivante, voyons comment ajouter des styles à une application React. Il existe plusieurs façons de procéder et nous verrons les autres méthodes plus tard. Tout d'abord, nous allons ajouter CSS à notre application à l'ancienne ; dans un seul fichier sans utiliser de préprocesseur CSS (bien que ce ne soit pas tout à fait vrai comme nous le verrons plus tard).
Ajoutons un nouveau fichier index.css sous le répertoire src puis ajoutons-le à l'application en l'important dans le fichier index.js :
import './index.css'
Ajoutons la règle CSS suivante au fichier index.css :
h1 {
color: green;
}
Remarque : lorsque le contenu du fichier index.css change, React peut ne pas le remarquer automatiquement, vous devrez donc peut-être actualiser le navigateur pour voir vos modifications !
Les règles CSS comprennent des sélecteurs et des déclarations. Le sélecteur définit les éléments auxquels la règle doit être appliquée. Le sélecteur ci-dessus est h1, qui correspondra à toutes les balises d'en-tête h1 de notre application.
La déclaration définit la propriété color sur la valeur green.
Une règle CSS peut contenir un nombre arbitraire de propriétés. Modifions la règle précédente pour rendre le texte cursif, en définissant le style de police en italique :
h1 {
color: green;
font-style: italic;}
Il existe de nombreuses façons de faire correspondre des éléments en utilisant différents types de sélecteurs CSS.
Si nous voulions cibler, disons, chacune des notes avec nos styles, nous pourrions utiliser le sélecteur li, car toutes les notes sont enveloppées dans des balises li :
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important';
return (
<li>
{note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}
Ajoutons la règle suivante à notre feuille de style (puisque ma connaissance de la conception Web élégante est proche de zéro, les styles n'ont pas beaucoup de sens) :
li {
color: grey;
padding-top: 3px;
font-size: 15px;
}
L'utilisation de types d'éléments pour définir des règles CSS est légèrement problématique. Si notre application contenait d'autres balises li, la même règle de style leur serait également appliquée.
Si nous voulons appliquer notre style spécifiquement aux notes, il est préférable d'utiliser class selectors.
En HTML normal, les classes sont définies comme la valeur de l'attribut class :
<li class="note">some text...</li>
Dans React, nous devons utiliser l'attribut className au lieu de l'attribut class. Gardant cela à l'esprit, apportons les modifications suivantes à notre composant Note :
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important';
return (
<li className='note'> {note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}
Les sélecteurs de classe sont définis avec la syntaxe .classname :
.note {
color: grey;
padding-top: 5px;
font-size: 15px;
}
Si vous ajoutez maintenant d'autres éléments li à l'application, ils ne seront pas affectés par la règle de style ci-dessus.
Message d'erreur amélioré
Nous avons précédemment implémenté le message d'erreur qui s'affichait lorsque l'utilisateur tentait de modifier l'importance d'une note supprimée avec la méthode alert. Implémentons le message d'erreur comme son propre composant React.
Le composant est assez simple :
const Notification = ({ message }) => {
if (message === null) {
return null
}
return (
<div className='error'>
{message}
</div>
)
}
Si la valeur de la prop message est null, alors rien n'est rendu à l'écran, et dans d'autres cas, le message est rendu à l'intérieur d'un élément div.
Ajoutons un nouvel élément d'état appelé errorMessage au composant App. Initialisons-le avec un message d'erreur afin que nous puissions immédiatement tester notre composant :
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
const [errorMessage, setErrorMessage] = useState('some error happened...')
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} /> <div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all' }
</button>
</div>
// ...
</div>
)
}
Ajoutons ensuite une règle de style qui convient à un message d'erreur :
.error {
color: red;
background: lightgrey;
font-size: 20px;
border-style: solid;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
Nous sommes maintenant prêts à ajouter la logique pour afficher le message d'erreur. Modifions la fonction toggleImportanceOf de la manière suivante :
const toggleImportanceOf = id => {
const note = notes.find(n => n.id === id)
const changedNote = { ...note, important: !note.important }
noteService
.update(changedNote).then(returnedNote => {
setNotes(notes.map(note => note.id !== id ? note : returnedNote))
})
.catch(error => {
setErrorMessage( `Note '${note.content}' was already removed from server` ) setTimeout(() => { setErrorMessage(null) }, 5000) setNotes(notes.filter(n => n.id !== id))
})
}
Lorsque l'erreur se produit, nous ajoutons un message d'erreur descriptif à l'état errorMessage. En même temps, nous démarrons une minuterie, qui définira l'état errorMessage sur null après cinq secondes.
Le résultat ressemble à ceci :

Le code de l'état actuel de notre application se trouve sur la branche part2-7 sur GitHub.
Styles en ligne
React permet également d'écrire des styles directement dans le code en tant que inline-styles.
L'idée derrière la définition des styles en ligne est extrêmement simple. Tout composant ou élément React peut être fourni avec un ensemble de propriétés CSS en tant qu'objet JavaScript via l'attribut style.
Les règles CSS sont définies légèrement différemment dans JavaScript que dans les fichiers CSS normaux. Disons que nous voulions donner à un élément la couleur verte et une police en italique d'une taille de 16 pixels. En CSS, cela ressemblerait à ceci :
{
color: green;
font-style: italic;
font-size: 16px;
}
Mais en tant qu'objet de style en ligne React, cela ressemblerait à ceci :
{
color: 'green',
fontStyle: 'italic',
fontSize: 16
}
Chaque propriété CSS est définie comme une propriété distincte de l'objet JavaScript. Les valeurs numériques des pixels peuvent être simplement définies comme des nombres entiers. L'une des principales différences par rapport au CSS standard est que les propriétés CSS avec trait d'union (kebab case) sont écrites en camelCase.
Ensuite, nous pourrions ajouter un "bloc inférieur" à notre application en créant un composant Footer et en définissant les styles en ligne suivants pour celui-ci:
const Footer = () => { const footerStyle = { color: 'green', fontStyle: 'italic', fontSize: 16 } return ( <div style={footerStyle}> <br /> <em>Note app, Department of Computer Science, University of Helsinki 2022</em> </div> )}
const App = () => {
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
// ...
<Footer /> </div>
)
}
Les styles en ligne comportent certaines limitations. Par exemple, les soi-disant pseudo-classes ne peuvent pas être utilisées directement.
Les styles en ligne et certaines des autres façons d'ajouter des styles aux composants React vont complètement à l'encontre des anciennes conventions. Traditionnellement, il a été considéré comme une bonne pratique de séparer entièrement CSS du contenu (HTML) et de la fonctionnalité (JavaScript). Selon cette ancienne école de pensée, l'objectif était d'écrire CSS, HTML et JavaScript dans leurs fichiers séparés.
La philosophie de React est, en fait, à l'opposé de cela. Étant donné que la séparation de CSS, HTML et JavaScript dans des fichiers séparés ne semblait pas bien évoluer dans les applications plus grandes, React fonde la division de l'application sur les lignes de ses entités fonctionnelles logiques.
Les unités structurelles qui composent les entités fonctionnelles de l'application sont des composants React. Un composant React définit le HTML pour structurer le contenu, les fonctions JavaScript pour déterminer les fonctionnalités, ainsi que le style du composant ; tout en un seul endroit. Il s'agit de créer des composants individuels aussi indépendants et réutilisables que possible.
Le code de la version finale de notre application se trouve sur la branche part2-8 sur GitHub.
Quelques remarques importantes
À la fin de cette partie, vous trouverez quelques exercices plus difficiles. À ce stade, vous pouvez les ignorer s'ils vous semblent trop complexes. Nous y reviendrons plus tard. Quoi qu'il en soit, ce document mérite d'être lu.
Nous avons fait, dans notre application, quelque chose qui masque une source d'erreur très courante.
Nous avons initialisé l'etat de notes à partir d'un tableau vide:
const App = () => {
const [notes, setNotes] = useState([])
// ...
}
C'est une valeur initiale tout à fait naturelle, puisque notes constitue un ensemble, c'est-à-dire qu'il y a de nombreuses notes que l'état va stocker.
Si l'état ne devait sauvegarder qu' " une seule chose ", une valeur initiale plus appropriée serait null, ce qui indique qu'il n'y a rien dans l'état au départ. Voyons ce qui se passe si nous utilisons cette valeur initiale :
const App = () => {
const [notes, setNotes] = useState(null)
// ...
}
L'application plante:

Le message d'erreur indique la raison et l'emplacement de l'erreur. Le code qui a provoqué le problème est le suivant :
// notesToShow gets the value of notes
const notesToShow = showAll
? notes
: notes.filter(note => note.important)
// ...
{notesToShow.map(note => <Note key={note.id} note={note} />
)}
Le message d'erreur est:
Cannot read properties of null (reading 'map')
La variable notesToShow est d'abord assigné à la valeur de l'état notes puis le code tente d'appeler la méthode map sur un object inexistant, c'est à dire sur null.
Pourquoi ?
Le hook d'effet(useEffect) utilise la fonction setNotes pour affecter à notes les données renvoyées par le back-end
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes) })
}, [])
Cependant, le problème est que le hook d'effet n'est exécuté qu'après le premier rendu. Et puisque notes a pour valeur initiale null:
const App = () => {
const [notes, setNotes] = useState(null)
// ...
Lors du premier rendu, le code suivant est exécuté :
notesToShow = notes
// ...
notesToShow.map(note => ...)
et cela fait planter l'application, car on ne peut pas appeler la méthode map sur une valeur null.
En initialisant notes avec un tableau vide, il n'y a pas d'erreur puisqu'il est permis d'utiliser map sur un tableau vide
Ainsi, l'initialisation de l'état a "masqué" le problème lié au fait que les données ne sont pas encore récupérées depuis le backend
une autre manière de contourner le problème aurait été d'utiliser un rendu conditionnel et de retourner null tant que l'état du composant n'est pas correctement initialisé :
const App = () => {
const [notes, setNotes] = useState(null) // ...
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
// do not render anything if notes is still null
if (!notes) { return null }
// ...
}
Donc, lors du premier rendu, rien n'est affiché. Lorsque les notes arrivent du backend, l'effet utilise la fonction setNotes our mettre à jour la valeur de l'état notes. Cela provoque un nouveau rendu du composant, et lors de ce second rendu, les notes sont affichées à l'écran.
Cette méthode basée sur le rendu conditionnel convient dans les cas où il est impossible de définir l'état de façon à permettre un rendu initial.
L'autre point auquel il nous faut encore jeter un œil est le second paramètre de useEffect:
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
Le second paramètre de useEffect sert à spécifier la fréquence d'exécution de l'effet. Le principe est que l'effet s'exécute systématiquement après le premier rendu du composant et à chaque fois que la valeur de ce second paramètre change.
Si ce second paramètre est un tableau vide [], son contenu ne change jamais et l'effet n'est exécuté qu'après le premier rendu du composant. C'est exactement ce que l'on souhaite lorsqu'on initialise l'état de l'application depuis le serveur.
Cependant, il existe des situations où l'on veut exécuter cet effet à d'autres moments, par exemple lorsque l'état du composant change de manière particulière.
Considérons l'exemple d'une application simple pour interroger les taux de change via l'Api de taux de change:
import { useState, useEffect } from 'react'
import axios from 'axios'
const App = () => {
const [value, setValue] = useState('')
const [rates, setRates] = useState({})
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// skip if currency is not defined
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency])
const handleChange = (event) => {
setValue(event.target.value)
}
const onSearch = (event) => {
event.preventDefault()
setCurrency(value)
}
return (
<div>
<form onSubmit={onSearch}>
currency: <input value={value} onChange={handleChange} />
<button type="submit">exchange rate</button>
</form>
<pre>
{JSON.stringify(rates, null, 2)}
</pre>
</div>
)
}
export default App
L'interface utilisateur de l'application comporte un formulaire dont le champ de saisie reçoit le code de la devise désirée. Si la devise existe, l'application affiche ses taux de change par rapport aux autres monnaies :

Lorsque l’on appuie sur le bouton, l’application stocke la devise saisie dans l’état currency.
Dès que la valeur de currency change, l’application récupère ses taux de change depuis l’API dans la fonction d’effet :
const App = () => {
// ...
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// skip if currency is not defined
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency]) // ...
}
Le hook useEffect prend désormais [currency] comme second paramètre. La fonction d’effet s’exécute donc après le premier rendu et à chaque fois que la valeur de ce second paramètre [currency] change. Autrement dit, lorsque l’état currency reçoit une nouvelle valeur, le contenu du tableau est mis à jour et la fonction d’effet est relancée.
Il est normal de choisir null comme valeur initiae pour la variable currency, puisque currency ne stocke qu'un seul élément. la valeur null indique qu’il n’y a encore rien dans l’état, et il est très simple, à l’aide d’un if, de vérifier si la variable a reçu une valeur. L’effet comporte donc la condition suivante :
if (currency) {
// exchange rates are fetched
}
ce qui empêche de requêter les taux de change juste après le premier rendu lorsque la variable currency a encore sa valeur initiale, c’est-à-dire null.
Ainsi, si l’utilisateur saisit par exemple eur dans le champ de recherche, l’application utilise Axios pour effectuer une requête HTTP GET vers l’adresse https://open.er-api.com/v6/latest/eur et stocke la réponse dans l’état rates.
Lorsque l’utilisateur saisit ensuite une autre valeur dans le champ de recherche, par exemple usd, la fonction d’effet est de nouveau exécutée et les taux de change de la nouvelle devise sont récupérés depuis l’API.
La méthode présentée ici pour effectuer les requêtes API peut sembler un peu lourde. Cette application particulière aurait pu être réalisée entièrement sans utiliser useEffect en effectuant les requêtes directement dans le gestionnaire de soumission du formulaire :
const onSearch = (event) => {
event.preventDefault()
axios
.get(`https://open.er-api.com/v6/latest/${value}`)
.then(response => {
setRates(response.data.rates)
})
}
Cependant, il existe des situations où cette technique ne fonctionnerait pas. Par exemple, vous pourriez rencontrer un tel cas dans l’exercice 2.20 où l’utilisation de useEffect pourrait apporter une solution. Notez que cela dépend beaucoup de l’approche choisie ; par exemple, la solution modèle n’utilise pas toujours cette astuce.