Aller au contenu principal

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 useEffect s'exécute ;
  • maîtriser le tableau de dépendances ([], [id], absent) ;
  • écrire la fonction de nettoyage (return () => ...) ;
  • charger des données avec async/await dans 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...

  1. 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
  2. appuyer sur « Recommencer » et dire quels états sont remis à leur valeur initiale
  3. écrire la ligne qui ajoute "azerty" à la fin de l'état historique sans 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 :

ÉcritureL'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.

Bonne pratique - Tout ce qui démarre doit s'arrêter

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 :

  1. l'effet est lancé pour id = 1 (requête lente) ;
  2. id change : React exécute le nettoyage (abort de la requête 1), puis relance l'effet pour id = 2 ;
  3. la requête 1 est rompue avec une AbortError, ignorée par le catch ;
  4. 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}
Une vraie requête fetch

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.

DOMException dans React Native

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​


Quand s'exécute useEffect(() => { ... }, []) ?


Quand s'exécute useEffect(effet, [id]) ?


À quoi sert la fonction renvoyée par un effet (return () => ...) ?


Pourquoi ne peut-on pas écrire useEffect(async () => { ... }) ?


Que fait le nettoyage controleur.abort() quand id passe de 1 à 2 ?


Dans quel cas faut-il mieux ne PAS utiliser un effet ?



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).


Bonne pratique - Un état qui commence par « 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.


Bonne pratique - Remettre « chargement » au début de l'effet

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.


Bonne pratique - Une annulation n'est pas une erreur

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.


Bonne pratique - Le tableau de dépendances n'est pas facultatif

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 :

  1. 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 ;
  2. appuyez sur Question 9 : le message Question 9 introuvable s'affiche en rouge ;
  3. 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​

NotionRésumé
EffetCode 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).
NettoyageLa fonction renvoyée par l'effet arrête ce qui a démarré (minuterie, requête).
async dans un effetDéfinir une fonction async à l'intérieur de l'effet, puis l'appeler.
Requêtes concurrentesLe nettoyage annule la requête précédente (AbortController) ; l'AbortError est ignorée.
État du chargementUn seul état chargement / succes / erreur, sans contradiction possible.
À éviterUn 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.