TP2 - Weather widget - Partie 1
Introduction
Bienvenue dans ce second TP.
L'objectif est de réaliser un "widget" météo qui affiche la météo actuelle ainsi que les prévisions pour les prochaines heures, ainsi que pour les prochains jours.
Pour ce faire, vous allez devoir entre autres, aller chercher des informations à l'aide d'une API, ici Open-Meteo.
Vous pouvez voir le résultat sur cette page.
Mise en place
Vite
Ouvrez un terminal dans le dossier dans lequel vous souhaitez créez votre application, puis tapez la commande suivante pour créer un projet Vite :
npm create vite@latest react-tp2 -- --template react
npm create est une commande permettant de générer un paquet npm vierge en utilisant un générateur contenu dans un paquet npm, en l'occurence, Vite. Le paquet en question (create-vite) sera téléchargé, utilisé, mais pas installé dans votre projet.
Vous pouvez accepter toutes les options par défaut qui vous sont proposées.
Si tout s'est correctement déroulé, la commande devrait avoir créé un dossier avec le nom de votre projet (ici react-tp2), installé les dépendances nécessaires à son fonctionnement, préparé quelques fichiers d'exemple, et lancé le serveur de développement.
À l'avenir quand vous voudrez travailler sur ce projet, vous pourrez re-lancer le serveur de développement en utilisant la commande suivante :
npm run dev
Projet Gitlab
Créez maintenant un projet react-tp2 sur le Gitlab de l'université.
Pensez bien à décocher l'option proposant de créer un commit initial !
Copiez l'adresse de ce nouveau repository à l'aide du bouton "Cloner avec SSH", puis exécutez les commandes suivantes dans le dossier de votre application crée à l'étape précédente en remplaçant <git-repo> par cette dernière :
# On initialise le dépôt Git local
git init
# On ajoute les fichiers existants au dépôt
git add .
# On effectue notre tout premier commit
git commit -m "Initial commit"
# On ajoute le projet créé précédemment comme distante
git remote add origin <git-repo>
# On finit par pousser notre commit sur le Gitlab de l'université !
git push -u origin main
Vous devriez maintenant voir sur le README.md de votre projet sur le Gitlab de l'université la documentation générée par create-vite.
Starter code
Vous pouvez désormais enlever tous les fichiers du dossier src créés par Vite, et décompresser l'archive suivante à la place.
Séparer les composants
Comme lors du TP précédent, nous pouvons commencer par séparer quelques composants du code d'origine.
Le composant TemperatureDisplay
Commencez par vous occuper de la partie qui affiche les trois températures : la moyenne, le max et le min.
Extrayez cette partie dans un nouveau composant TemperatureDisplay, dans un nouveau fichier src/components/TemperatureDisplay.jsx.
Ce composant devra pouvoir être utilisé de cette manière :
<TemperatureDisplay
min={18}
max={25}
avg={22}
/>
Correction
const TemperatureDisplay = props => {
const {
avg,
min,
max
} = props
return <div className="temperature-display">
<p className="temperature-display-avg">{
avg
}</p>
<div className="temperature-display-row">
<p>{
max
}</p>
<p className="temperature-display-row-item--min">{
min
}</p>
</div>
</div>
}
Le composant WeatherCode
Extrayons la balise img qui affiche l'image du soleil avec un nuage dans un nouveau composant WeatherCode.
Comme vous avez pu le constater, ce composant a pour fonction d'afficher une image représentant le code météo qu'il reçoit. La liste de ces codes météos est détaillée dans la documentation d'Open-Meteo.
Actuellement, le composant affiche une image dont l'URL est marquée en dur dans la balise img.
Téléchargez le pack d'images suivant et décompressez-le dans un dossier src/assets.
Grace à la configuration de faite par Vite, nous pouvons importer dans nos fichiers des images locales, que nous recevront dans Javascript comme des URL.
Vous pouvez par exemple utiliser une image de la sorte :
import partialSunIcon from '../assets/partial-sun.png'
const WeatherCode = props => {
// ...
return <img
src={partialIcon}
className="weathercode-img"
alt="Logo partiellement nuageux"
/>
}
Modifiez donc vous-même le composant WeatherCode de manière à lui faire afficher une image différente en fonction de sa prop code, c'est à dire de manière à pouvoir être utilisé de cette façon :
<WeatherCode code={51} />
Pour vous aider, voici une liste des correspondances entre les codes et les images fournies :
| Code | Image |
|---|---|
| 0 | sunshine.png |
| 2 | partial-sun.png |
| 3 | clouds.png |
| 45 | fog.png |
| 51 | sun-rain.png |
| 65 | heavy-rain.png |
| 71 | slight-snow.png |
| 75 | heavy-snow.png |
| 80 | heavy-rain.png |
| 85 | heavy-snow.png |
| 95 | thunderstorm.png |
Une fois ceci terminé, faites un commit avec le commentaire "Ex 1 : Splitting components" et poussez vers le serveur.
Correction
import React from 'react'
import PropTypes from 'prop-types'
import cloudsIcon from '../assets/img/clouds.png'
import fogIcon from '../assets/img/fog.png'
import heavyRainIcon from '../assets/img/heavy-rain.png'
import heavySnowIcon from '../assets/img/heavy-snow.png'
import partialSunIcon from '../assets/img/partial-sun.png'
import slightSnowIcon from '../assets/img/slight-snow.png'
import sunRainIcon from '../assets/img/sun-rain.png'
import sunshineIcon from '../assets/img/sunshine.png'
import thunderstormIcon from '../assets/img/thunderstorm.png'
const codes = [{
code: 95,
image: thunderstormIcon
}, {
code: 85,
image: heavySnowIcon
}, {
code: 80,
image: heavyRainIcon
}, {
code: 75,
image: heavySnowIcon
}, {
code: 71,
image: slightSnowIcon
}, {
code: 65,
image: heavyRainIcon
}, {
code: 51,
image: sunRainIcon
}, {
code: 45,
image: fogIcon
}, {
code: 3,
image: cloudsIcon
}, {
code: 2,
image: partialSunIcon
}, {
code: 0,
image: sunshineIcon
}]
const findImg = code => codes
.find(i => code >= i.code)
?.image
const WeatherCode = props => {
const {
code
} = props
return <img
src={findImg(code)}
alt="Weather logo"
className="weathercode-img"
/>
}
git add .
git commit -m 'Ex 1 : Splitting components'
git push
PropTypes
Avec React, il existe plusieurs outils permettant de faciliter la vie du développeur, dont les prop types.
Il s'agit d'une technique pour préciser à React quelle est le type (Array, Boolean, String, ...) d'une certaine prop qu'un composant doit recevoir. Si on a défini ces prop types, React affichera un message d'avertissement dans la console (en mode développement uniquement) si on passe une prop d'un mauvais type à un de nos composant.
Voici un exemple avec le composant WeatherCode :
import PropTypes from 'prop-types'
const WeatherCode = props => {
// ...
}
WeatherCode.propTypes = {
code: PropTypes.number.isRequired
}
Commencez par installer le paquet requis dans votre projet :
npm install prop-types
Essayez après avoir ajouté ce code à votre composant WeatherCode de lui passer une chaîne de caractères en prop code. Observez le warning dans votre console.
C'est très pratique pour éviter certaines erreurs simple, que vous utilisiez vos propres composants, ou encore ceux de quelqu'un d'autre.
Vous avez peut-être entendu parler de TypeScript, un language fortement typé qui compile en Javascript ? C'est une autre technique qui permet de valider (de manière bien plus drastique) le type des props d'un composant React. Si le sujet vous intéresse, je vous invite à vous rensigner dans la documentation officielle.
Pour l'heure, ajoutez-donc des prop types à tous les composants dont vous disposez qui en ont besoin.
Une fois ceci terminé, faites un commit avec le commentaire "Ex 2 : Using typechecking" et poussez vers le serveur.
Correction
// ...
TemperatureDisplay.propTypes = {
avg: PropTypes.number.isRequired,
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired
}
git add .
git commit -m 'Ex 2 : Using typechecking'
git push
Utiliser une API HTTP
Maintenant que nous disposons de nos différents composants capables d'être interactifs, nous avons besoin de données pour afficher des informations intéressantes dans notre application.
Open-Meteo
Dans le cadre de ce TP nous allons utiliser une API (Application Programming Interface) HTTP pour aller chercher des données météo pour notre petite application.
Il existe plusieurs API permettant d'avoir accès à des informations météo gratuitement. Je vous propose d'utiliser Open-Meteo, qui a l'avantage d'être open source et sans authentification. Vous pouvez accéder à la documentation de cette API sur cette page.
Essayez de comprendre par vous même comment cette API fonctionne et comment construire l'URL d'appel.
Faire une requête GET avec fetch
Pour récupérer ces données, nous allons faire une requête AJAX.
Le terme AJAX signifie "Asynchronous Javascript And XML", et désigne en fait une technique visant à aller chercher des données de manière asynchrone sur une page web. Le XML n'est plus beaucoup utilisé pour ce faire, on lui préfère le JSON qui fait partie de Javascript et qui est plus léger. C'est avec du JSON que nous allons travailler aujourd'hui.
Il existe plusieurs moyens de faire des requêtes AJAX en Javascript. Nous allons pour ce TP utiliser la technique la plus moderne, et la plus pratique à utiliser : fetch.
Une démonstration vaut mieux qu'un grand discours.
function FetchDemo(){ const [fact, setFact] = useState(null) const getData = () => { fetch('https://uselessfacts.jsph.pl/random.json?language=en') .then(res => res.json()) .then(data => setFact(data)) } return <div> {fact && <figure> <blockquote> {fact.text} </blockquote> <cite> {fact.source} </cite> </figure>} <button onClick={getData}> Get a random fact </button> </div> }
Comme vous pouvez le voir dans cet exemple, la fonction fetch est appelée quand l'utilisateur clique sur le bouton.
Cette fonction retourne une Promise, contenant un objet Response dont j'appelle ensuite la méthode Response#json. Cette méthode retourne elle aussi une Promise, je récupère donc la donnée initialement reçue en JSON déchiffrée dans un objet Javascript dans le Promise#then suivant.
A l'aide de cet exemple, faites en sorte d'effectuer une requête adaptée vers l'adresse de l'API d'Open-Meteo lorsque l'utilisateur clique sur le bouton rafraîchir. Contentez-vous pour le moment de stocker le résultat de cet appel dans un state dans votre composant App.
Correction
// Je commence par mettre toutes les infos dans des constantes globales au fichier
const baseUrl = 'https://api.open-meteo.com/v1/forecast'
const timezone = 'Europe/London'
const dailyVars = [
'weathercode',
'temperature_2m_max',
'temperature_2m_min'
]
const hourlyVars = [
'temperature_2m',
'weathercode'
]
// Les coordonnées de La Rochelle ;-)
const latitude = 46.1592
const longitude = -1.171
const App = () => {
const [state, setState] = useState({data: null})
const getData = () => {
fetch(`${baseUrl}?latitude=${latitude}&longitude=${longitude}&hourly=${hourlyVars.join(',')}&daily=${dailyVars.join(',')}&timezone=${timezone}`)
.then(res => res.json())
.then(data => {
setState({
data
})
})
}
return <main className="weather-container">
{/* ... */}
<button className="refresh-button" onClick={getData}>
{/* ... */}
</main>
}
Une fois ceci terminé, faites un commit avec le commentaire "Ex 3 : Fetching data" et poussez vers le serveur.
git add .
git commit -m 'Ex 3 : Fetching data'
git push
Afficher la date de mise à jour
Vous avez peut-être remarqué que notre widget dispose d'un petit footer, dans lequel est affiché une date.
Modifiez votre fonction getData (si vous l'avez appelée comme ça) ainsi que la structure de votre state de manière à stocker le timestamp du moment auquel vous recevez la réponse de l'API.
On peut obtenir le timestamp de l'instant actuel à l'aide de le fonction Date.now()
Faites en sorte ensuite d'afficher de manière formatée l'heure de la dernière mise à jour dans le footer.
Une fois ceci terminé, faites un commit avec le commentaire "Ex 4 : Displaying date" et poussez vers le serveur.
git add .
git commit -m 'Ex 4 : Displaying date'
git push
Afficher les données dans nos composants visuels
Maintenant que nos données de météo sont dans notre state, on peut peut transférer les valeurs adaptées à nos composants d'affichage.
Essayez de passer les données adaptées aux composants TemperatureDisplay et WeatherCode. Vous allez vite vous rendre compte d'un problème : tant que la donnée n'existe pas encore, vous ne pouvez pas y accéder. Affichez donc plutôt tant que vous n'avez pas la donnée un petit texte type "Pas de données.
Une fois ceci terminé, faites un commit avec le commentaire "Ex 5 : Displaying data" et poussez vers le serveur.
git add .
git commit -m 'Ex 5 : Displaying data'
git push
Mise à jour des données
Nous arrivons maintenant à aller chercher des données au clic sur le bouton rafraîchir et à les afficher.
Cependant, il serait souhaitable que l'application aille chercher ces données dès le début.
useEffect et cycle de vie
Pour pouvoir effectuer une action au premier affichage d'un composant, on utilise le hook useEffect.
Cette fonction a de nombreuses fonctionnalités. Pour commencer, utilisons la de manière simple, comme dans l'exemple suivant.
const HelloEffect = () => {
useEffect(() => {
alert('Je suis affiché !')
}, [])
return <p>
Hello
</p>
}
function DemoEffect(){ const [present, setPresent] = useState(false) return <div> <fieldset> <input id="present-checkbox" type="checkbox" checked={present} onChange={() => setPresent(!present)} /> <label htmlFor="present-checkbox"> Présent </label> </fieldset> {present && <HelloMountEffect />} </div> }
Comme vous pouvez le voir, l'alerte se produit une fois que le composant est monté sur le DOM.
À l'aide de cet exemple, essayez de faire en sorte que la requête AJAX se fasse dès l'arrivée de notre widget sur l'écran.
N'oubliez pas de passer un Array vide comme second argument à useEffect ! Nous verrons son utilité lors de la second partie de ce TP.
Pour le moment, vous pouvez ignorer le warning dans votre console de serveur de développement. Nous en reparlerons dans la seconde partie de ce TP.
Correction
const App = () => {
// ...
useEffect(() => {
getData()
}, [])
// ...
}
Mettre à jour régulièrement
Maintenant, notre application affiche des données dès le début. Mais la météo, ça change régulièrement... L'idéal serait que notre application mette à jour ses données toute seule de manière régulière.
Imaginons que notre application ait plusieurs pages. On ne souhaite pas que la requète AJAX continue de s'effectuer toutes les 10 secondes si notre composant n'est plus affiché à l'écran.
Vous savez déjà comment vous servir de la fonction setInterval. Vous savez aussi comment vous servir de la fonction clearInterval pour stopper l'appel régulier mis en place.
Le hook useEffect propose aussi une réponse à ce problème. Regardez l'exemple suivant.
import React, { useEffect } from 'react'
const HelloUnmountEffect = () => {
useEffect(() => {
return () => {
alert('Je suis détruit !')
}
}, [])
return <p>
Hello again
</p>
}
function DemoEffect(){ const [present, setPresent] = useState(false) return <div> <fieldset> <input id="present-checkbox" type="checkbox" checked={present} onChange={() => setPresent(!present)} /> <label htmlFor="present-checkbox"> Présent </label> </fieldset> {present && <HelloUnmountEffect />} </div> }
Comme vous pouvez le constater, la fonction retournée par la fonction passée au hook useEffect est appelée lorsque le composant est démonté du DOM.
Forts de ces nouvelles connaissances, vous pouvez maintenant faire en sorte que notre application se mette à jour toutes les 10 secondes !
Correction
const App = () => {
// ...
useEffect(() => {
getData()
const timer = setInterval(getData, 10000)
return () => {
clearInterval(timer)
}
}, [])
// ...
}
Une fois ceci terminé, faites un commit avec le commentaire "Ex 6 : Updating data" et poussez vers le serveur.
git add .
git commit -m 'Ex 6 : Updating data'
git push
Avec React, suivant les cas d'utilisation, il arrive (régulièrement) que des composants soient créés et détruits par centaines, voire milliers. Dans ce type de situation, ces composants doivent être irréprochables dans leur gestion de la mémoire. Pensez dès maintenant à nettoyer les effets d'un composant quand vous développez, cela vous évitera certainement ces soucis plus tard.
Composants classe
Depuis le début de ce cours, nous avons uniquement utilisé des composants fonctions.
Aujourd'hui, on utilise quasi-exclusivement cette syntaxe, qui est plus compacte, plus moderne et moins complexe, mais il vous arrivera peut-être de croiser une autre syntaxe utilisée précédemment : les composants classe.
Voici une exemple de composant classe :
import React, { Component } from 'react'
class Hello extends Component {
render(){
return <p>
Hello {this.props.name} !
</p>
}
}
Comme vous pouvez le constater, ces composants (comme leur nom l'indique) sont en fait des classes Javascript, qui héritent de la classe React.Component.
Dans les composants fonction, on retourne le code JSX de notre affichage à la fin de la fonction. Dans un composant classe, on utilise pour ce faire une méthode render.
À la différence des composants fonction, notre méthode render ne reçoit pas directement les props comme argument, mais directement comme propriété de l'instance (donc accessible par this).
Voyons comment reproduire les fonctionnalités que nous connaissions jusqu'ici avec des composants classe.
Le state
Avec les composants fonction, vous savez utiliser useState pour créer un nouveau state.
Avec les composants classe, voici comment reprendre l'exemple du compteur de la première présentation :
class Counter extends React.Component { constructor(props){ super(props) this.state = { count: 0 } } handleClick(){ this.setState({ count: this.state.count + 1 }) } render(){ return <section> <p>Compteur : {this.state.count}</p> <button onClick={() => this.handleClick()}> +1 </button> </section> } }
Comme vous pouvez le constater, on ne peut avoir qu'un seul state par composant qui est attribué à la propriété this.state. On définit sa valeur initiale dans le constructeur.
Pour ensuite modifier ce state, on fait un appel à la méthode setState.
Cycle de vie
Avec les composants fonction, on peut réagir en fonction du cycle de vie d'un composant à l'aide du hook useEffect.
Dans un composant classe, on peut réagir aux évènements du cycle de vie d'un composant dans ce qu'on appelle des lifecycle hooks (à ne pas confondre avec les hooks : useState, useEffect...). Ce sont des méthodes de classe portant des noms spécifiques.
class LifeCycle extends React.Component { componentDidMount(){ console.log('Je suis monté sur le DOM !') } componentWillUnmount(){ console.log('Je suis démonté du DOM... !') } render(){ return <p>Hello</p> } }
A vous !
Je sais, ce n'est pas très amusant de réécrire du code... Mais essayez de transformer le composant TemperatureDisplay en composant classe, pour la science !
Cette syntaxe est encore utilisée dans un très grand nombre d'applications en production aujourd'hui, et n'est pas du tout dépréciée. Il est donc important que vous sachiez la lire et l'écrire.
Une fois ceci terminé, faites un commit avec le commentaire "Ex 7 : Class components" et poussez vers le serveur.
git add .
git commit -m 'Ex 7 : Class components'
git push
Félicitations d'en être arrivés jusqu'ici, vous disposez maintenant d'un petit widget météo assez fonctionnel. Il nous reste encore pas mal de fonctionnalités à y ajouter, mais ce sera pour la suite !