Aller au contenu principal

Laboratoire 4: Application météo

Introduction

Dans ce labo, nous verrons comment alimenter notre application React avec des données venant d’une API. Nous verrons aussi les méthodes de cycles de vie qui permettent de récupérer les données sans bloquer le rendu de notre composant. Le but sera de construire une application qui permet de :

  • Comparer la météo entre 2 lieux
  • Voir la météo d’un lieu grâce à une URL
Un présentateur de la météo sur un fond coloré montrant des cartes du temps.

Prérequis

Vous devrez aussi faire des requêtes avec votre application vers une API. Pour cela, vous aurez deux moyens de le faire: via axios ou via un fetch (natif). L’API que nous utiliserons pour récupérer les données se nomme : Open Weather Map. Vous aurez besoin d’une clé pour pouvoir utiliser leur API. Vous avez besoin de vous inscrire pour en obtenir une. Nous utiliserons le plan GRATUIT pour faire le labo. Si vous rencontrez des difficultés pour récupérer votre clé, vous pouvez utiliser la documentation suivante : https://openweathermap.org/appid#get La documentation de l’API est disponible à cette adresse : https://openweathermap.org/current. Vous pourrez y découvrir les paramètres à mettre dans votre requête, mais également comment interroger l’API (définir les unités pour la température ou choisir la langue de la réponse, par exemple).

Envoyer une requête à L’API

Maintenant que vous avez votre clé, vous aimeriez pouvoir faire une requête à l’API. Dans un premier temps, vous pouvez utiliser Postman pour pouvoir tester rapidement. Cependant, vous devrez le faire en JavaScript pour que votre application puisse communiquer avec l’API. Pour cela, vous avez 2 options :

  • La méthode fetch native de JavaScript
  • Utiliser un module comme axios

La première approche est actuellement supportée par presque tous les navigateurs web. Vous aurez donc besoin de consulter sa documentation pour voir son fonctionnement. Cependant, la méthode fetch se tient au minimum et vous devrez gérer vous-mêmes les paramètres de la requête. Vous pourrez vous aider de URLSearchParams qui permet de générer facilement les paramètres de la requête. Voici la documentation qui pourra vous aider (et une réponse StackOverflow) :

La seconde méthode consiste à utiliser un module nommé Axios qui permet de générer rapidement des requêtes grâce à un objet de configuration que vous passerez au constructeur. Si vous désirez utiliser cette approche vous devriez consulter la documentation du module à l’adresse suivante :

useTransition()

Le hook useTransition est un hook qui répond à une problématique de la mise à jour d'un état en React: la synchronicité. En effet, quand vous mettez un état à jour, cette opération est synchrone. Dans la plupart des cas, vous n'aurez aucun problème. Le problème est par contre plus voyant si une des mises à jour est longue. Dès lors, elle va bloquer la file d'attente des rendus de React et ainsi également l'interface.

Dans cet exemple, si vous cliquez sur Articles (lent), puis directement sur Contact, vous verrez que la page de contact met du temps à s'afficher.

Source

Quand vous avez effectuez votre premier click sur Articles (lent), il y a eun premier rendu lent qui a bloqué la file d'attente de React. Comme vous avez appuyez sur Contact, mais que le premier rendu était toujours en cours, ce dernier s'est retrouvé coincé dans cette fille d'attente.

La solution a ce problème consiste à avoir une approche asynchrone.

Dans ce deuxième exemple, si vous cliquez sur Articles (lent), puis directement sur Contact, vous verrez que la page de contact s'affiche directement.

Pour arriver à ce résultat, il nous faut un nouveau hook: useTransition. Voici sa signature:

const [isPending, startTransition] = useTransition()

useTransition ne prend aucun paramètre et renvoie un tableau avec deux éléments: un booléan qui indique si la transition est en cours et une fonction permettant d'enclencher la transition. La transition consiste simplement à effectuer un changement d'état de manière asynchrone.

Pour effectuer correctement notre changement d'état, nous devons effectuer ce dernier dans un appel à startTransition en le passant en callback (fonction synchrone uniquement).

starTransition(() => {
setState(...) // le changement est effectué dans le starTransition
});
attention

Votre changement d'état doit être appelé "pendant" l'appel à starTransition !

Votre code ne fonctionnera pas si vous faites ceci:

startTransition(() => {
setTimeout(() => {
setState(...) // le changement d'état n'est plus effectué dans le startTransition !
}, 200);
});

Le problème vient de l'ordre d'exécution du thread JavaScript.

  1. startTransition commence
  2. le timer est créé
  3. startTransition se termine (il ne consite qu'à créer un timer)
  4. le timer se termine
  5. setState est exécuté, car le timer se termine

L'approche correcte est la suivante:

setTimeout(() => {
startTransition(() => {
setState(...)
})
}, 200);

Ce qui nous donne:

  1. le timer est créé
  2. le timer se termine
  3. startTransition commence
  4. setState est exécuté
  5. startTransition se termine

Un deuxième cas problème concerne les promesses qui souffrent du même problème: l'ordre d'exécution Votre code ne fonctionne pas si vous faites ceci:

startTransition(async () => {
await fonctionAsynchrone();
setState(...); // le changement d'état n'est plus effectué dans le startTransition !
})

Voici l'ordre d'éxécution:

  1. startTransition commence
  2. la fonction anonyme crée une promesse
  3. startTransition se termine
  4. la promesse progresse (l'await a fini de bloquer)
  5. setState est exécuté

La version correcte devrait être ceci:

await fonctionAsynchrone();
startTransition(() => {
setState(...);
})

Voici l'ordre d'éxécution:

  1. fonctionAsynchrone bloque (await)
  2. fonctionAsynchrone est terminé, donc on progresse
  3. startTransition commence
  4. setState est exécuté
  5. startTransition se termine
remarque

C'est pour cette raison que startTransition doit prendre une fonction synchrone comme argument !

That's why. That's exacly why.

Enfin, finisons sur un petit point de vocabulaire. L'ensemble des fonctions appelées dans un startTransition sont appelées Action. D'ailleurs, il est conseillé d'utiliser le mot Action dans le nom des fonctions utilisées dans le startTransition.

danger

Si vous utilisez des fonctions pouvant renvoyer des erreurs, pensez à utiliser des try/cacth ! Sinon l'erreur pourrait se propager (si vous n'avez aucun mécanisme de contrôle d'exception) et faire planter votre application !

useActionState()

Le hook useActionState permet d'effectuer des changements d'états en utilisant une Action. Voici sa signature:

const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);

Commençons par les valeurs de retour:

  • State est l'état renvoyé par useActionState lors de l'initialisation ou l'état renvoyé par reducerAction
  • dispatchAction est une fonction qui permet d'envoyer une action à reducerAction
  • isPending est un boolean qui indique si la transition est en cours ou non.

Maintenant, les paramètres de useActionState:

  • reducerAction est une fonction (synchrone ou asynchrone) qui va recevoir 2 paramètres:
    • l'état précédent
    • l'action et cette fonction renverra un état en appliquant l'action à l'état précédent
  • l'état initial
  • permalink est paramètre optionnnel utilisé si vous utilisez le rendu côté serveur, ce qui n'est pas notre cas. Vous pouvez donc l'oublier.
remarque

Cela ne vous rappelle pas Redux ?

dispatchAction doit être utilisé dans une Action (voir le hook précédent) ou dans une propriété action dans certains éléments. C'est ce dernier point qui va nous intéresser. En effet, l'envoie d'un formulaire est parfois un peu fastidieux. Vous devez:

  1. préparer le formulaire (au niveau du code)
  2. récupérer les données du formulaire
  3. préparer les données pour l'envoie
  4. envoyer les données
  5. indiquer à l'utilisateur qu'on attend la réponse du serveur
  6. réceptionner la réponse du serveur
  7. traiter la réponse du serveur
  8. Mettre à jour l'état du composant
  9. indiquer à l'utilisateur qu'on a terminé avec l'envoie du formulaire

Notre useActionState va nous permettre de gérer quelques points pour nous, ainsi que la propriété action.

export default function Search() {
function search(formData) {
const query = formData.get("query");
alert(`You searched for '${query}'`);
}
return (
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
);
}

Source

Dans ce code, nous voyons que passons search comme valeur de la propriété action au niveau du formulaire.

astuce

Vous ne voyez pas d'appel à e.preventDefault(). C'est normal, React le fait pour nous, car nous utilisons la propriété action.

Ici, nous voyons que React va récupérer les données du formulaire et les mettre dans un formData pour nous. Nous n'en discuterons pas dans ce laboratoire, car ce dernier sera utilisé et présenté dans le prochain. En effet, l'API que nous allons utiliser ne s'attend pas à recevoir un formData.

Par contre, nous pouvons nous amuser à combiner ce code avec le hook useActionState pour rendre le formulaire plus dynamique.

export default function Search() {
const [state, dispatchAction, isPending] = useActionState(async (previousState, actionPayload) => {
try {
// appels à des fonctions
return {...} // votre nouveau state
} catch (err) {
// gestion des erreurs
return {...} //votre nouveau state
}
}, initialState);


function search(formData) {
const query = formData.get("query");
alert(`You searched for '${query}'`);
}

return (
<form action={search}>
<input name="query" />
<button type="submit" disable={isPending}>Search</button>
</form>
);
}

Ce qui nous donne ceci:

  1. préparer le formulaire (au niveau du code)
  2. récupérer les données du formulaire (fait par React)
  3. préparer les données pour l'envoie (fait par React)
  4. envoyer les données (fait par React)
  5. indiquer à l'utilisateur qu'on attend la réponse du serveur (en partie par React)
  6. réceptionner la réponse du serveur
  7. traiter la réponse du serveur
  8. Mettre à jour l'état du composant
  9. indiquer à l'utilisateur qu'on a terminé avec l'envoie du formulaire (fait par React)

Vous voyez donc qu'une bonne partie est gérée par React et vous facilitera la tâche. Evidemment, nous pousserons ce hook plus loin dans le prochain laboratoire.

useEffect()

Ce troisième hook va nous permettre de rajouter un "effet" à notre composant. Il s'agit d'une fonction qui sera enclenchée à chaque fois qu'une des dépendances sera modifiée. useEffect(setup, dependencies?) prend deux paramètres: la fonction de setup et un tableau de variables représentant les dépendances (optionnel). Il va nous permettre de gérer tout ce qui est "externe" à React, notamment les appels à l'API !

La fonction de setup est une fonction représentant l'action à effectuer. Le comportement de l'effet variera en fonction de la valeur de retour de cette fonction ! Si la fonction ne renvoie rien, l'effet est juste appliqué. Par contre, si la fonction renvoie une autre fonction, alors cette dernière sera utilisée avant chaque rendu (sauf la première fois que le composant est rendu). Cette "autre fonction" est appelée fonction de nettoyage et permet de supprimer les ressources utilisées et non controllées par React: un websocktet, un timer, un interval, une animation, une ressource d'une librairie, etc. La première approche sera utilisée lorsque que la ressource ne persiste pas (exemple: une requête à une API), tandis que la seconde sera utilisée dans les autres cas.

Voici un exemple où le composant ouvre une connexion et cette connexion persiste (pour recevoir des messages en "temps réel"). Il faut supprimer la connexion quand le composant est démonté (sinon, elle restera ouverte tant que l'utilisateur ne ferme pas l'onglet):

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => { // Fonction de nettoyage
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

Source

Le deuxième paramètre est une liste de dépendances. Le principe est simple: React va surveiller chacune des variables présentes dans le tableau. Dès qu'une des variables change de valeur, le useEffect va s'enclencher. Par contre, si vous ne précisez rien, l'effet s'enclenchera à chaque nouveau rendu de votre composant. Si vous mettez un tableau vide, alors l'effet ne s'enclenchera qu'au premier rendu de votre composant (lorsqu'il est créé).

astuce

En résumé:

Tableau de dépendancesSignification
Tableau videL'effet ne s'enclenchera qu'à l'apparition de votre composant sur la page web (montage)
Aucun tableaul'effet s'enlenchera à tous les rendus de votre composant
Tableau avec des variablesl'effet s'enclenchera à l'apparition de votre composant sur la page web ET à chaque fois qu'une des variables de votre tableau changera de valeur

Et le cycle de vie ?

Comme pour les composants avec les classes, on peut représenter le cycle de vie avec les hooks ! Le voici: Diagramme du cycle de vie des hooks React, incluant les phases de montage, de mise à jour, d&#39;engagement et de nettoyage, avec des explications sur chaque étape et les hooks associés.

Source

Exercice

Maintenant que vous avez les informations nécessaires disponibles, il est temps de passer à l’exercice. Vous devrez faire 2 vues :

  • Une première qui permet de comparer la météo de 2 villes
  • Une deuxième qui affiche seulement la météo d’une ville précisée dans l’URL

Vous devrez également gérer :

  • Si l’utilisateur tape le nom d’une ville inconnue
  • Un affichage spécifique durant les temps de chargement

Enfin, il vous est demandé de faire du code "propre". Il y a beaucoup de points communs entre les 2 vues. A vous de faire en sorte de ne pas dupliquer inutilement du code. Ne vous préoccupez pas de la "beauté" de votre application, ce n’est pas le but de l’exercice. Voici à quoi vous devriez arrivés : Pour la comparaison de ville :

Prévisions météo pour Wavre et Namur : ciel dégagé, 27° à Wavre et 28° à Namur.

Si on tape l’URL avec le nom de la ville (ex : /city/namur) :

Météo à Namur : ciel dégagé, température de 28°.