Les effets avec useEffect
Charger des données et lancer une minuterie au bon moment, et surtout nettoyer derrière soi.
Objectifs de la séance
- comprendre ce qu'est un effet et quand
useEffects'exécute ; - maîtriser le tableau de dépendances (
[],[id], absent) ; - écrire la fonction de nettoyage (
return () => ...) ; - charger des données avec
async/awaitdans un effet ; - annuler une requête devenue inutile et ignorer une réponse périmée ;
- représenter le chargement avec un état unique (
chargement,succes,erreur).
Petite vérification
Avant de commencer, vérifiez si vous savez...
- dans Expo Snack, avec le code de la séance précédente, appuyer deux fois de suite sur la bonne réponse et lire le score
- appuyer sur « Recommencer » et dire quels états sont remis à leur valeur initiale
- écrire la ligne qui ajoute
"azerty"à la fin de l'étathistoriquesans le modifier
📌 Réponse attendue
Notions théoriques
Qu'est-ce qu'un effet ?
Un composant a un travail principal : décrire l'affichage à partir de l'état et des props, de façon pure. Mais une application doit aussi faire des choses en dehors de l'affichage :
- charger des données depuis un serveur ;
- lancer une minuterie ;
- s'abonner à un événement.
Ces actions s'appellent des effets. On les place dans useEffect, qui s'exécute après l'affichage du composant.
import { useEffect } from "react";
useEffect(() => {
console.log("Le composant vient d'être affiché");
}, []);
Le tableau de dépendances
Le deuxième argument de useEffect indique quand l'effet doit s'exécuter :
| Écriture | L'effet s'exécute... |
|---|---|
useEffect(effet, []) | une seule fois, après le premier affichage |
useEffect(effet, [id]) | après le premier affichage, puis chaque fois que id change |
useEffect(effet) (sans tableau) | après chaque affichage (presque toujours une erreur) |
Règle : le tableau doit lister toutes les variables (états, props) que l'effet utilise. Si l'effet utilise id, écrivez [id] : sinon l'effet utiliserait une valeur périmée (l'idée des fermetures de la partie 4).
La fonction de nettoyage
Un effet peut renvoyer une fonction : c'est le nettoyage. React l'exécute :
- avant de relancer l'effet (quand une dépendance change) ;
- quand le composant disparaît de l'écran.
Exemple classique, un chronomètre :
const [secondes, setSecondes] = useState(0);
useEffect(() => {
const minuterie = setInterval(() => {
setSecondes((ancien) => ancien + 1); // mise à jour fonctionnelle : jamais de valeur périmée
}, 1000);
return () => {
clearInterval(minuterie); // nettoyage : on arrête la minuterie
};
}, []);
Sans le nettoyage, la minuterie continuerait à tourner après la disparition de l'écran, et mettrait à jour un composant qui n'existe plus.
Chaque fois qu'un effet démarre quelque chose (minuterie, requête, abonnement), demandez-vous : « qui l'arrête ? ». La réponse est la fonction de nettoyage.
Charger des données dans un effet
useEffect ne peut pas recevoir directement une fonction async (elle renverrait une promesse au lieu d'un nettoyage). On définit donc une fonction async à l'intérieur de l'effet, puis on l'appelle.
useEffect(() => {
async function charger() {
const question = await chargerQuestion(id);
setQuestion(question);
}
charger();
}, [id]);
Le problème : les réponses dans le désordre
Reprenez la situation de la partie 5 : le joueur appuie sur « Question 1 » (serveur lent : 1,5 s), puis immédiatement sur « Question 2 » (rapide : 0,3 s). Sans précaution, c'est la réponse de la question 1, qui arrive en dernier, qui s'affiche : l'écran montre la mauvaise question.
La solution est celle étudiée en partie 5 : un AbortController. À chaque changement de id, le nettoyage annule la requête précédente.
useEffect(() => {
const controleur = new AbortController();
async function charger() {
setEtat({ statut: "chargement" });
try {
const question = await chargerQuestion(id, controleur.signal);
setEtat({ statut: "succes", question });
} catch (erreur) {
if (erreur instanceof Error && erreur.name === "AbortError") {
return; // annulation volontaire : on n'affiche rien
}
setEtat({ statut: "erreur", message: messageErreur(erreur) });
}
}
charger();
return () => {
controleur.abort(); // nettoyage : on annule la requête devenue inutile
};
}, [id]);
Le déroulement quand id passe de 1 à 2 :
- l'effet est lancé pour
id = 1(requête lente) ; idchange : React exécute le nettoyage (abortde la requête 1), puis relance l'effet pourid = 2;- la requête 1 est rompue avec une
AbortError, ignorée par lecatch; - seule la requête 2 met à jour l'état.
Un seul état pour le chargement
Plutôt que trois états séparés (enChargement, erreur, question) qui peuvent se contredire, on utilise l'union de la partie 6 :
type Etat =
| { statut: "chargement" }
| { statut: "succes"; question: Question }
| { statut: "erreur"; message: string };
const [etat, setEtat] = useState<Etat>({ statut: "chargement" });
Et l'affichage choisit selon le statut :
{etat.statut === "chargement" ? <ActivityIndicator size="large" /> : null}
{etat.statut === "succes" ? <Text>{etat.question.enonce}</Text> : null}
{etat.statut === "erreur" ? <Text>{etat.message}</Text> : null}
Avec un vrai serveur, le schéma est le même : on passe le signal à fetch (fetch(url, { signal: controleur.signal })), on teste reponse.ok, puis on lit le JSON comme unknown et on le valide
(partie 6) avant de le ranger dans l'état.
Dans nos exercices, la requête annulée est rompue avec une simple Error dont on règle le name sur "AbortError". L'objet DOMException, utilisé dans la partie 5 avec Node.js, n'existe pas toujours dans React Native.
Quand ne PAS utiliser un effet
- réagir à un appui : écrivez le code dans la fonction appelée par
onPress, pas dans un effet ; - calculer une valeur à partir de l'état (un pourcentage, une liste filtrée) : calculez-la directement pendant le rendu, sans effet ni état supplémentaire.
Un effet sert uniquement à synchroniser le composant avec le monde extérieur (réseau, minuterie, abonnement).
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Votre mission : charger une question depuis un « serveur » simulé, en gérant le chargement, l'erreur et l'annulation. Dans Expo Snack, remplacez le contenu de App.tsx.
Point de départ (le « serveur » simulé, les types et l'affichage sont fournis ; vous écrivez l'effet) :
import { useEffect, useState } from "react";
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
type Question = {
id: number;
enonce: string;
};
type Etat =
| { statut: "chargement" }
| { statut: "succes"; question: Question }
| { statut: "erreur"; message: string };
// « Serveur » simulé : la question 1 est lente (1,5 s), les autres rapides (0,3 s), au-delà de 3 : erreur.
function chargerQuestion(id: number, signal: AbortSignal): Promise<Question> {
return new Promise((resolve, reject) => {
const delai = id === 1 ? 1500 : 300;
const minuterie = setTimeout(() => {
if (id > 3) {
reject(new Error(`Question ${id} introuvable`));
return;
}
resolve({ id, enonce: `Énoncé de la question ${id}` });
}, delai);
signal.addEventListener("abort", () => {
clearTimeout(minuterie);
const annulation = new Error("Chargement annulé");
annulation.name = "AbortError";
reject(annulation);
});
});
}
function messageErreur(erreur: unknown): string {
return erreur instanceof Error ? erreur.message : "Erreur inconnue";
}
Étape 1 — Les deux états du composant
Déclarez l'id de la question affichée (valeur initiale 1) et l'état du chargement (valeur initiale : chargement).
Au premier affichage, les données n'existent pas encore : l'état initial est « chargement », jamais un tableau vide qui ferait croire à l'utilisateur qu'il n'y a rien à afficher.
Étape 2 — Un contrôleur d'annulation
Dans useEffect, créez l'AbortController, puis passez son signal à chargerQuestion.
Quand id change, l'ancienne question est encore affichée : setEtat({ statut: "chargement" }) en début d'effet affiche tout de suite le cercle de chargement, au lieu d'une question périmée.
Étape 3 — Traiter l'échec, mais pas l'annulation
Complétez le catch : une annulation est ignorée, toute autre erreur est enregistrée dans l'état.
Si l'annulation affichait un message d'erreur, l'utilisateur verrait « Chargement annulé » à chaque changement de question. On la laisse passer en silence.
Étape 4 — Le nettoyage et les dépendances
Terminez l'effet : appelez charger(), annulez la requête dans le nettoyage, et indiquez que l'effet dépend de id.
L'effet utilise id : id doit figurer dans le tableau. Oublier une dépendance donne un affichage qui ne se met pas à jour, ou, pire, qui utilise une valeur périmée.
Pour tester votre application dans l'aperçu, ajoutez l'affichage ci-dessous dans le return de App : trois boutons pour choisir la question, et un affichage selon le statut.
return (
<View style={styles.ecran}>
<View style={styles.rangee}>
<Pressable style={styles.bouton} onPress={() => setId(1)}>
<Text>Question 1 (lente)</Text>
</Pressable>
<Pressable style={styles.bouton} onPress={() => setId(2)}>
<Text>Question 2 (rapide)</Text>
</Pressable>
<Pressable style={styles.bouton} onPress={() => setId(9)}>
<Text>Question 9 (absente)</Text>
</Pressable>
</View>
{etat.statut === "chargement" ? <ActivityIndicator size="large" /> : null}
{etat.statut === "succes" ? <Text style={styles.texte}>{etat.question.enonce}</Text> : null}
{etat.statut === "erreur" ? <Text style={styles.erreur}>{etat.message}</Text> : null}
</View>
);
Testez ces trois scénarios dans l'aperçu :
- appuyez sur Question 1 puis, aussitôt, sur Question 2 : seule la question 2 doit s'afficher, et rien ne doit changer 1,5 s plus tard ;
- appuyez sur Question 9 : le message
Question 9 introuvables'affiche en rouge ; - appuyez sur Question 1 et attendez : le cercle de chargement tourne 1,5 s, puis la question s'affiche.
📌 Une solution
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
| Effet | Code exécuté après l'affichage, pour se synchroniser avec l'extérieur (réseau, minuterie). |
| Tableau de dépendances | [] une fois, [id] quand id change, absent : à chaque rendu (à éviter). |
| Nettoyage | La fonction renvoyée par l'effet arrête ce qui a démarré (minuterie, requête). |
async dans un effet | Définir une fonction async à l'intérieur de l'effet, puis l'appeler. |
| Requêtes concurrentes | Le nettoyage annule la requête précédente (AbortController) ; l'AbortError est ignorée. |
| État du chargement | Un seul état chargement / succes / erreur, sans contradiction possible. |
| À éviter | Un effet pour réagir à un clic ou pour calculer une valeur dérivée. |
Aperçu de la prochaine séance
Vous avez maintenant vu les trois piliers de React : composants, état, effets. Dans la dernière séance, vous allez tout assembler dans un mini-quiz complet, et dresser le bilan de ce que vous savez faire avant d'attaquer la série Expo.