TP2 - Weather widget - Partie 2
#
Onglets et prévisionsVous l'avez peut-être remarqué : une section
est cachée ( à l'aide de la classe hidden
) en base de l'application. Enlevez-lui cette classe pour la faire apparaître.
Comme vous pouvez le voir, il s'agit d'onglets, suivis d'une liste horizontale de prévisions pour la journée.
#
Le composant ForecastItemComme la partie représentant une plage horaire est dupliquée avec peu de changement, c'est la candidate idéale pour être extraite dans un nouveau composant.
Extrayez donc cette partie dans un nouveau composant nommé ForecastItem
. Il devra être utilisé de la sorte :
<ForecastItem label="10h" code={55} temperature={21}/>
Une fois ceci fait, remplacez dans le code de votre composant App
les différentes plages horaires par ce nouveau composant, mais ne l'écrivez pas cinq fois. Utilisez un Array
pour l'afficher 5 fois avec de fausses données (comme ci-dessus).
tip
Le constructeur de l'objet Array
peut prendre comme argument un nombre représentant la longueur de l'Array
à créer. Il suffit ensuite de le remplir de null
par exemple à l'aide de la méthode Array#fill
pour pouvoir itérer dessus avec Array#map
...
Correction
const ForecastItem = props => { const { code, label, temperature } = props
return <li className="forecast-item"> <p> {label} </p> <WeatherCode code={code}/> <p className="forecast-item-temp"> {temperature} </p> </li>}
ForecastItem.propTypes = { code: PropTypes.number.isRequired, label: PropTypes.string.isRequired, temperature: PropTypes.number.isRequired}
const App = () => { // ... return <main className="weather-container"> {/* ... */} <ul className="forecast"> {Array(5).fill(null).map((i, idx) => <ForecastItem key={idx} label="10h" code={55} temperature={21} />)} </ul> {/* ... */} </main>}
#
Choisir son ongletActuellement, on ne peut selectionner un onglet : le clic n'a aucun effet.
Notre application doit se souvenir de l'onglet actuellement sélectionné, et donner à cet onglet la classe tab--active
.
Faites ces modifications.
Correction
const App = () => { // ... const [currentTab, setCurrentTab] = useState('day') // ... return <main className="weather-container"> {/* ... */} <nav className="tabs"> <button onClick={() => setCurrentTab('day')} className={currentTab === 'day' ? 'tab tab--active' : 'tab' }> Journée </button> <button onClick={() => setSelectedTab('week')} className={currentTab === 'week' ? 'tab tab--active' : 'tab' }> Semaine </button> </nav> {/* ... */} </main>}
#
Afficher les données apropriéesVous l'aurez compris : l'objectif est d'afficher les données heure par heure quand l'onglet "Journée" est sélectionné, et jour par jour pour l'onglet "Semaine".
Utilisez les données de l'API OpenMeteo pour passer les données apropriées aux éléments ForecastItem
. Pour vérifier le résultat obtenu, n'hésitez pas à aller voir le resultat.
tip
Pensez à bien lire la documentation de l'objet Date
, et à jouer avec l'index de l'itération en cours (second argument reçu par la fonction passée à Array#map
...)
Une fois ceci terminé, faites un commit avec le commentaire "Ex 8 : Switching tabs" et poussez vers le serveur.
git add .git commit -m 'Ex 8 : Switching tabs'git push
Correction
const App = () => { // ... return <main className="weather-container"> {/* ... */} {state.data && <section> {/* ... */} <ul className="forecast"> {currentTab === 'week' && Array(5).fill(null) .map((i, idx) => <ForecastItem key={idx} code={state.data.daily.weathercode[idx+1]} label={new Date(state.data.daily.time[idx+1]) .toLocaleDateString().slice(0, -5) } temperature={ ((state.data.daily.temperature_2m_max[idx+1] + state.data.daily.temperature_2m_min[idx+1])/2).toFixed(1) } />) } {currentTab === 'day' && Array(5).fill(null) .map((i, idx) => <ForecastItem key={idx} code={state.data.hourly.weathercode[6+(idx*4)]} label={`${6+(idx*4)}h`} temperature={state.data.hourly.temperature_2m[6+(idx*4)]} />) } </ul> </section>} {/* ... */} </main>}
#
Changer d'endroitNotre widget météo est maintenant assez complet ! Parfait pour nous, rochelais !.. Mais il serait quand même pratique de pouvoir sélectionner une autre ville.
WeatherWidget
#
Extraire un composant Notre application doit désormais être constituée d'un widget météo, ainsi que d'une barre de recherche.
Actuellement, le code du widget météo est situé intégralement dans le composant App
.
Extrayez ce code dans un composant WeatherWidget
, que vous placerez dans un nouveau fichier src/components/WeatherWidget.jsx
.
Adaptez votre composant App
pour utiliser ce composant nouvellement créé.
Correction
const App = () => {
return <main className="weather-container"> <WeatherWidget /> </main>}
Search
#
Le composant Nous allons mettre en place une barre de recherche avec autocomplétion, afin de simplifier l'utilisation de notre application : à chaque frappe, l'utilisateur se verra proposé une liste de villes, qu'il pourra ensuite sélectionner.
Voici le code JSX d'une barre de recherche :
import React from 'react'
const Search = () => {
return <div className="searchbar-container">
<div className="searchbar-input-group"> <input type="text" className="searchbar-input" placeholder="Rechercher..." value="La Rochelle" /> <button className="searchbar-button"> <svg className="searchbar-button-logo" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z" /> </svg> </button> </div> <div className="hidden searchbar-options"> <ul> <li> <button> La Rochelle (17000) </button> </li> </ul> </div> </div>}
export default Search
Placez ce code dans un nouveau fichier src/components/Search.jsx
.
Intégrez ce nouveau composant dans votre composant App
de manière à afficher la barre de recherche au dessus de votre widget météo.
Modifiez ensuite le composant Search
pour faire de l'input
un élément contrôlé.
Correction
const Search = () => { const [inputValue, setInputValue] = useState('') return <div className="searchbar-container"> {/* ... */} <input type="text" className="searchbar-input" placeholder="Rechercher..." value={inputValue} onChange={e => setInputValue(e.target.value)} /> {/* ... */} </div>}
#
Chercher une villeIl existe différent moyens de réaliser une barre de recherche avec autocomplétion pour trouver un nom de ville. Je vous propose aujourd'hui d'utiliser une API publiée par le gouvernement français : l'API geo.
Comme vous pouvez le voir dans sa documentation, cette API permet entre autres de faire une recherche de ville par nom, ce qui nous intéresse, mais surtout elle permet d'obtenir les coordonnées (latitude et longitude) du point central de la ville, donnée dont nous avons besoin je vous le rappelle pour aller chercher les prévisions d'OpenMeteo.
Commencez par créer une fonction getData
comme nous l'avions fait pour l'API d'OpenMeteo, qui fait un appel à fetch
sur l'URL appropriée, avant de sauvegarder le résultat dans un state.
Dans le code du composant Search fourni précédemment, vous pouvez observer qu'il y a une liste cachée à l'aide de la classe hidden
.
C'est la liste des options à choisir pour l'utilisateur. Modifiez ces éléments pour afficher les données reçues de la recherche.
Maintenant que l'utilisateur se voit afficher des options, il faut qu'il puisse en choisir une. Modifiez le composant Search
de manière à ce qu'il puisse être utilisé de la sorte dans le composant App
:
{/* ... */}<Search onSelect={city => console.log(city)}/>{/* ... */}
Correction
const baseUrl = 'https://geo.api.gouv.fr'
const Search = props => {
const { onSelect } = props
const [inputValue, setInputValue] = useState('') const [list, setList] = useState([])
const getData = () => { fetch(`${baseUrl}/communes?nom=${inputValue}&fields=centre,departement&boost=population&limit=5`) .then(res => res.json()) .then(data => setList(data)) }
const handleSelect = item => { onSelect(item) setList([]) setInputValue('') }
return <div className="searchbar-container"> {/* ... */} <input type="text" className="searchbar-input" placeholder="Rechercher..." value={inputValue} onChange={e => setInputValue(e.target.value)} /> <button className="searchbar-button" onClick={getData}> {/* ... */} </button> {/* ... */} {!!list.length && <div className="searchbar-options"> <ul> {list.map(searchItem => <li key={searchItem.code}> <button onClick={() => handleSelect(searchItem)}> {searchItem.nom} ({searchItem.departement.code}) </button> </li>)} </ul> </div>} </div>}
WeatherWidget
#
Passer la ville sélectionnée à Maintenant que notre utilisateur peut rechercher et sélectionner une ville, il faut passer cette information au composant WeatherWidget
.
Pour commencer, il faut stocker l'information reçue (la ville sélectionnée) du composant Search
dans le composant App
afin de pouvoir passer les informations nécessaires en props au composant WeatherWidget
.
Modifiez également le composant WeatherWidget
de manière à utiliser les coordonnées reçues par ses props lors de son appel à OpenMeteo. N'oubliez pas d'afficher le bon nom de ville ! Voici les PropTypes
de ce composant :
WeatherWidget.propTypes = { latitude: Proptypes.number.isRequired, longitude: Proptypes.number.isRequired, cityName: PropType.string.isRequired}
Correction
const App = () => {
const [location, setLocation] = useState({ centre: { coordinates: [ -1.171, 46.1592 ] }, nom: 'La Rochelle' })
const [longitude, latitude] = location.centre.coordinates
return <main className="weather-container"> <Search onSelect={setLocation} /> <WeatherWidget cityName={location.nom} latitude={latitude} longitude={longitude} /> </main>}
Comme vous pouvez le constater, les données ne se mettent pas à jour sans cliquer sur le bouton. Nous allons remédier à cela dans la section suivante. Mais d'abord, une petite partie théorie.
Une fois ceci terminé, faites un commit avec le commentaire "Ex 9 : Changing places" et poussez vers le serveur.
git add .git commit -m 'Ex 9 : Changing places'git push
#
HooksNous avons utilisé deux fonctions spéciales de React : useState
et useEffect
. Ces dernières permettant respectivement de garder un état dans notre composant, et de réagir à des évènements de cycle de vie du composant.
Ces fonctions sont nommées des hooks, il en existe quelques autres dans React, et un nombre incalculable dans la communauté. Ils ont été introduits dans React 16.8 afin de permettre l'utilisation de states et autres fonctionnalités de React dans les composants fonction, et sont depuis devenus le nouveau standard, bien que les composants classes ne soient pas du tout dépréciés.
#
Les règles des hooksLes hooks ne sont ni plus ni moins que des fonctions Javascript. Cependant, ils doivent respecter ces règles :
- Ils ne peuvent être appelés qu'en début de composant, et tels quels : il ne faut pas les appeler dans des conditions ou dans des boucles ou autres fonctions.
- Ils ne peuvent être utilisés que dans des composants fonctions
info
Si vous voulez savoir pourquoi ces règles existent, je vous encourage à aller lire la page correspondante de la documentation de React.
useEffect
#
Les dépendances de Vous avez peut-être remarqué le warning dans les logs de votre serveur de développement vous disant qu'il manque une "dépendance" à votre appel à useEffect
dans votre composant WeatherWidget
.
Nous avons utilisé le hook useEffect
en lui passant comme second argument un Array
vide. Cet Array
représente en fait les "dépendances" de la fonction qui est passée, c'est à dire les variables utilisées dans cette dernière.
La fonction useEffect
permet d'exécuter une fonction au montage ou au démontage d'un composant, mais pas seulement : elle permet aussi d'executer cette fonction dès qu'une des dépendances fournies change.
Exemple :
SyntaxError: Unexpected token (1:8) 1 : return () ^
Comme vous pouvez le constater dans l'exemple ci-dessus, l'action de cocher ou décocher la case à cocher déclenche l'affichage d'un message dans la console disant que checked
a changé, mais aussi que le fonction de nettoyage est appelée.
Modifiez l'appel à useEffect
dans votre composant WeatherWidget
de manière à ce que la requête AJAX soit renouvelée immédiatement quand le composant reçoit de nouvelles coordonnées par ses props.
Correction
// ...useEffect(() => { getData() const timer = setInterval(getData, 60000) return () => { clearInterval(timer) }}, [longitude, latitude])// ...
useEffect
#
Mettre au propre le Normalement, même en ayant ajouté les coordonnées passées en props comme dépendances à useEffect
, vous devriez toujours voir le warning dans la console de votre serveur de développement, disant que vous devez avoir votre fonction getData
dans les dépendances de votre appel à useEffect
.
#
Le problèmeEssayez de l'ajouter. Vous allez (très) vite vous rendre compte que cela pose un problème : le composant tourne en boucle infinie.
En effet, la fonction getData
étant créée dans votre composant, elle est re-créée à chaque affichage. Cette dépendance ayant changé, useEffect
s'execute à nouveau, fait la requête AJAX et met à jour le state
, ce qui a pour effet de déclencher un nouvel affichage, et donc la création d'une nouvelle fonction getData
, et ainsi de suite.
Nous pourrions enlever cette fonction du composant et la mettre à la racine de notre fichier, mais elle n'aurait plus accès au state
, et perdrait de son intérêt.
#
La solutionÉvidemment, React propose un utilitaire pour gérer ce genre de problèmes : le hook useCallback
.
Ce hook prend en argument une fonction, suivie d'un Array
de dépendances, et retourne une nouvelle fonction.
La fonction retournée est dite "mémorisée" (memoized en anglais), c'est à dire qu'elle ne change pas à chaque appel : elle ne change que si l'une des dépendances a changé.
Utilisez useCallback
pour créer votre fonction getData
, afin de générer une nouvelle fonction quand les coordonnées changent. Vous pouvez ensuite utiliser votre fonction getData
comme unique dépendance pour votre appel à useEffect
.
Correction
// ...const getData = useCallback(() => { fetch(`...`) .then(res => res.json()) .then(data => { setState({ data, lastUpdate: Date.now() }) })}, [longitude, latitude])
useEffect(() => { getData() const timer = setInterval(getData, 60000) return () => { clearInterval(timer) }}, [getData])//...
Maintenant, dès que les coordonnées changent, la fonction getData
est régénérée, donc la fonction passée à useEffect
s'exécute à nouveau.
Search
#
Dans Notre composant Search
fonctionne, mais il serait nettement plus pratique si il pouvait aller chercher les données tout seul au fur et à mesure de notre saisie.
À l'aide des hooks useEffect
et useCallback
, modifiez votre composant Search
de manière à faire une nouvelle requête AJAX vers l'API géo si l'utilisateur a entré plus de trois caractères.
Correction
// ...const getData = useCallback(() => { fetch(`...`) .then(res => res.json()) .then(data => setList(data))}, [inputValue])
useEffect(() => { if(inputValue.length >= 3){ getData() }}, [getData, inputValue.length])// ...
Une fois ceci terminé, faites un commit avec le commentaire "Ex 10 : Memoized callbacks" et poussez vers le serveur.
git add .git commit -m 'Ex 10 : Memoized callbacks'git push
#
Écrire son propre hookCe qui rend les hooks vraiment intéressants et très appréciés de la communauté, c'est la possibilité de créer ses propres hooks à partir de ceux fournis par React.
Avec React, le code est séparé en composants, qui contiennent à la fois la partie logique (les states, effects, event handlers, callbacks...) et la partie graphique (HTML et CSS). Il est ainsi facile de réutiliser ces composants.
Cependant, si vous souhaitez réutiliser uniquement la partie graphique d'un composant, c'est très simple : il vous suffit d'en séparer la partie visuelle et d'en faire un composant complètement contrôlé, sans logique. Pour réutiliser la logique d'un composant, il suffit d'encapsuler la logique de notre composant dans un hook, qui pourra être réutilisé avec un visuel différent.
Ces hooks une foix extraits d'un composant, ils sont facilement réutilisables dans différents projets React. Vous verrez assez rapidement d'ailleurs que les hooks produits par la communauté pullulent sur NPM.
Voici un exemple simpliste de hook custom :
const useCounter = (start=0, increment=1) => { const [counter, setCounter] = useState(start) const add = () => { setCounter(counter+increment) } return [counter, add]}
SyntaxError: Unexpected token (1:8) 1 : return () ^
Comme vous pouvez le voir, la logique de gestion d'état du compteur a été extraite dans un hook séparé nommé useCounter
.
Revenons-en à nos moutons : nous allons créer un hook qui gère la partie logique de notre composant WeatherWidget, c'est à dire la requête AJAX, le state et la logique de cycle de vie.
Extrayez cette logique de votre composant pour la mettre dans un hook useOpenMeteo
, que vous mettrez dans un nouveau fichier src/hooks/useOpenMeteo.js
.
tip
N'oubliez pas qu'un hook est avant tout une fonction qui peut prendre des paramètres, comme pour useCounter
par exemple, et qui peut renvoyer n'importe quoi : un Array
, un objet...
Correction
const useOpenMeteo = (settings) => { const { latitude, longitude, hourlyVars, dailyVars, timezone, interval } = settings
const [state, setState] = useState({ data: null, lastUpdate: null })
const refresh = useCallback( () => { fetch(`${baseUrl}?latitude=${latitude}&longitude=${longitude}&hourly=${hourlyVars.join(',')}&daily=${dailyVars.join(',')}&timezone=${timezone}`) .then(res => res.json()) .then(data => { setState({ data, lastUpdate: Date.now() }) }) }, [ latitude, longitude, hourlyVars, dailyVars, timezone ] )
useEffect(() => { refresh() const timer = setInterval(() => { refresh() }, interval)
return () => { clearInterval(timer) } }, [interval, refresh])
return { ...state, refresh }}
// ...const { refresh, lastUpdate, data} = useWeather({ timezone, dailyVars, hourlyVars, latitude, longitude, interval: 60000})// ...
Une fois ceci terminé, faites un commit avec le commentaire "Ex 11 : Writing custom hooks" et poussez vers le serveur.
git add .git commit -m 'Ex 11 : Writing custom hooks'git push
#
ConclusionFélicitations, vous êtes arrivés à la fin du second TP de cette série !
Listons les compétences acquises lors de ce TP :
- Utiliser des
PropTypes
- Faire un appel asynchrone à une API HTTP
- Utiliser le cycle de vie d'un composant React
- Lire et écrire des composants classe
- Créer nos propres hooks
Ces acquis vous permettent déjà de réaliser une grande majorité des fonctionnalités que vous pourriez souhaiter avoir dans une application web, c'est un immense pas sur votre chemin de futur développeur/euse React !
#
Pistes d'améliorationsVous en voulez encore ? Voici quelques idées de ce que vous pourriez améliorer dans notre widget, par ordre de difficulté :
- Ajouter la possibilité de modifier le délai de mise à jour des données
- Ajouter un mécanisme de debounce à la requête de la barre de recherche
- Sauvegarder la dernière ville choisie pour arriver directement dessus lors de la prochaine visite de l'utilisateur
- Créer un "gestionnaire" de widgets météo, permettant d'en avoir et d'en configurer plusieurs sur une même page
- Faire en sorte qu'une requête
fetch
en cours soit annulée si une autre est lancée dans le mêmeuseEffect
(voirAbortController
)
Votre imagination est la limite, il est possible de faire encore beaucoup d'autres choses avec ce widget !