Skip to main content

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#

Create React App#

Ouvrez un terminal dans le dossier dans lequel vous souhaitez créez votre application, puis tapez la commande suivante pour utiliser Create React App :

npx create-react-app react-tp2
note

npx est un outil en ligne de commande faisant partie de npm qui permet d'éxecuter directement un module npm sans l'installer.

Si tout s'est correctement déroulé, Create React App 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 créé un repository git local avec un premier commit.

Vous pouvez d'ores et déjà lancer le serveur de développement en utilisant la commande suivante :

npm run start

Projet Gitlab#

Créez maintenant un projet react-tp2 sur le Gitlab de l'université.

danger

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 :

git remote add origin <git-repo>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 React App.

Starter code#

Vous pouvez désormais enlever tous les fichiers du dossier src créés par Create React App, 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#

Commençons par s'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
src/components/TemperatureDisplay.jsx
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 Webpack faîte par Create React App, 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 :

src/components/WeatherCode.jsx
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 :

CodeImage
0sunshine.png
2partial-sun.png
3clouds.png
45fog.png
51sun-rain.png
65heavy-rain.png
71slight-snow.png
75heavy-snow.png
80heavy-rain.png
85heavy-snow.png
95thunderstorm.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}

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
src/components/TemperatureDisplay.jsx
// ...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.

Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

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
src/components/App.jsx
// Je commence par mettre toutes les infos dans des constantes globales au fichierconst 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.1592const 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.

tip

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>}
Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

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.

info

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
src/components/App.jsx
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>}
Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

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
src/components/App.jsx
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 :

Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

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.

Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

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 !