Aller au contenu principal


Le hook : useEffect

Un Hook est une fonction qui permet d' « accrocher » une des fonctionnalités React. à une fonction-composant.

Pour utiliser useEffet dans notre fonction-composant, il faut l'importer.

import React, {useEffect] from react

Billet créé le :
30 Sep 2022
Dernière MAJ :
09 jan 2025

On utilise le hook useEffect pour exécuter une fonction, dite callback (l'effet produit)  :

  • immédiatement après le rendu initial (le 1er rendu) de la fonction-composant ;

  • et lorsque l'état de la "fonction-composant" change si l' argument optionnel "dependencies" le permet .

La syntaxe à utiliser est la suivante :

useEffet(callback,[dependencies])

La fonction callback peut retourner (return) une fonction (dite de "nettoyage", par exemple pour éviter la saturation de mémoire) qui sera exécutée avant toute nouvelle exécution de la fonction callback.

L'argument optionnel "dependencies" est un tableau (array) qui conditionne la ré-exécution de la fonction "callback".

Lorsque le paramètre optionnel de dépendance est omis, l'effet callback de "useEffet" est ré-appelé après chaque rendu.
Lorsque le paramètre optionnel de dépendance est un tableau vide, l'effet n'est pas ré-appelé.
Lorsque le paramètre optionnel de dépendance est le tableau [arg1, arg2 , ...] ou arg1, arg2 sont des variables d'état ou des props de la fonction-composant, l' effet sera ré-appelé chaque fois que la valeur d'une des variables arg1 ou arg2 ou ... du tableau est modifiée.

"useEffect" conditionne donc l'exécution de l'effet. Mais n'oublions pas qu' à chaque modification de la valeur d'une variable d'état (voir le billet  : le hook useState), un nouveau rendu est exécuté. Ce rendu sera donc suivi ou pas de l'exécution de l'effet selon le tableau de dépendance de l'effet.

Exemple 1 : un GPS

La portion de code ci-dessous est extraite de l'onglet "le GPS" figurant au bas de cette page. On y met en évidence :

  1. les variables d'état utilisées pour mémoriser :

  • la position de l'utilisateur : location  ;

  • la route qu'il suit : route.

  1. les fonctions "callback" des hooks 'useEffect".

Elles sont en gras dans le code. Les effets (callback) sont :

  1. la recherche de la position actuelle (donc la modification de l'état "location" du composant GPS) par la fonction "requestInitLocation();" ;

  2. la mise à jour cette position (donc la modification de l'état "location" du composant GPS) par la fonction "locationUpdate();" ;

  3. la mise à jour de la route suivie.

const Gps = ({navigation}) => {
//
const [ location, setLocation ] = useState({longitude:2.1044, latitude: 48.8775);
const [route, setRoute] =useState([location]);
// .......

// recherche de la position actuelle
useEffect(
() => {
requestInitLocation();
} ,
[] ); //tableau de dépendance vide

// mise à jour cette position
useEffect(
() => {
watchID = locationUpdate();
return () => {
// fonction de nettoyage pour éviter la saturation de mémoire
() => {
Geolocation.clearWatch(watchID);
}
};
}, []); //tableau de dépendance vide

// mise à jour de la route suivie.
useEffect(
() => {
setRoute([
...route,{latitude:location.latitude, longitude:location.longitude}
]);
},
[location]); //tableau de dépendance non vide
//...

Observez :

  • un hook "useEffect" par action à réaliser (règle de bonne pratique) ;

  • la fonction clearWatch() appelée en retour de l'effet n°2 pour annuler l'action répétée de "watchPosition()" : c'est le "nettoyage" ;

  • l'utilisation de l'opérateur "spread" (...) pour mettre à jour le tableau "route" ;

  • des tableaux de dépendance vide dans les effets n°1 et n°2, qui ne seront donc exécutés qu'une seule fois (après le 1er affichage) ;

  • l'état "location" dans le tableau de dépendance de l'effet n°3, ce qui déclenchera un nouveau rendu à chaque modification de la position de l'utilisateur et une mise à jour de la route suivie.

L'image suivante illustre l'exécution de ce code.

  • Cliquez pour agrandir l'image

Cliquez sur le bouton pour copier le code dans le presse papier
 //import MapView from 'react-native-maps';
import MapView, { PROVIDER_GOOGLE, Marker,Polyline } from 'react-native-maps';
//import Geolocation
import Geolocation from '@react-native-community/geolocation';
import React, {useState, useEffect} from 'react';
import {
Image,
PermissionsAndroid,
Text,
StyleSheet,
View
} from 'react-native';
import { IconButton} from 'react-native-paper';
//
const LATITUDE = 48.8775;
const LONGITUDE = 2.1044;
const LATITUDE_DELTA = 0.006;
const LONGITUDE_DELTA = 0.006;
//
const Gps = ({navigation}) => {
//
const [location, setLocation] = useState({latitude:LATITUDE, longitude:LONGITUDE});
const [region, setRegion ] = useState (
{
longitude:LONGITUDE,
latitude: LATITUDE,
longitudeDelta: LONGITUDE_DELTA,
latitudeDelta: LATITUDE_DELTA,
}
);
//
const[ locationStatus, setLocationStatus] = useState('');
const [route, setRoute] =useState([location]);
//
/*
* https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition
* getCurrentPosition(success, error, options)
*/
//
const initLocation = () => {
Geolocation.getCurrentPosition(
(position) => {
let lat = position.coords.latitude;
let lon = position.coords.longitude;
setLocationStatus('initialisation : ');
setRegion({...region, latitude:lat,longitude:lon});
setLocation({longitude:lon, latitude:lat});
},
(error) => {
setLocationStatus(error.message);
},
{
enableHighAccuracy: true,
timeout: 10000, //temps max pour calculer la position
maximumAge: 1000 //recherche la position toutes les 1s, sinon lit le cache
},
);
};
/*
* https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition
* watchPosition(success, error, options)
*/
const locationUpdate =() => {
return(
Geolocation.watchPosition(
(position) => {
let lat = position.coords.latitude;
let lon = position.coords.longitude;
setRegion({...region, latitude:lat,longitude:lon});
setLocationStatus('mise à jour : ');
setLocation({...location, longitude:lon,latitude:lat});
},
(error) => {
setLocationStatus(error.message);
},
{
enableHighAccuracy: true,
distanceFilter:5,
timeout: 20000, //temps max pour calculer la position
maximumAge: 1000 //recherche la position toutes les 1s, sinon lit le cache
},
)
)};
//
const requestInitLocation = async () => {
if (Platform.OS === 'ios') {
currentLocation();
} else {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Access Requis',
message: "Cette App a besoin d'accèder à votre position",
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
initLocation();
} else {
setLocationStatus('Permission refusée');
}
}
catch (err) {
console.warn(err);
}
}
};
// initialisation de la position (execution unique)
useEffect(
() => {

requestInitLocation;
//setRoute([{latitude:location.latitude,longitude:location.longitude}]);
},
[]);
// mise à jour de la position (execution unique)
useEffect(
() => {
watchID = locationUpdate();
return () => {
// fonction de nettoyage pour aviter la saturation de mémoire
() => {
Geolocation.clearWatch(watchID);
}
};
},
[]);
// mise à jour de la route (execution à chaque modification de location)
useEffect(
() => {
setRoute([
...route,{latitude:location.latitude, longitude:location.longitude}
]);
console.log("route setRoute :" +route.length);
},
[location]);
//affichage
return (
<View style={styles.container}>
<Text style={styles.text}> {locationStatus} </Text>
<MapView
provider={PROVIDER_GOOGLE} // remove if not using Google Maps
style={styles.map}
showsUserLocation={true}
userLocationUpdateInterval = {1000}
followsUserLocation={true}
region={region}
customMapStyle={mapStyle}
>
{location && (
<Marker
coordinate={{
latitude: location.latitude,
longitude: location.longitude,
}}
pinColor = { '#96FF70'}
/>
)}
{route.length > 1 && (
<Polyline
coordinates={route}
strokeColor='#B24112'
strokeWidth={2}
/>
)}
</MapView>
{location && (
<View>
<Text style={styles.text}> latitude : {location.latitude} </Text>
<Text style={styles.text}>longitude : {location.longitude} </Text>
</View>
)}
<Text style={styles.text}> length : {route.length} </Text>
{/*<Text style={styles.text}> altitude : {Math.trunc(altitude * 10000) /10000} </Text>*/}
</View>
);
};
//
const mapStyle = [
{
featureType: 'administrative.locality',
elementType: 'labels.text.fill',
stylers: [{color: '#D33613'}],
},
{
featureType: 'poi',
elementType: 'labels',
stylers: [{ visibility: "off" }],
},
{
featureType: 'poi.park',
elementType: 'labels',
stylers: [{ visibility: "on" }],
},
{
featureType: 'road.local',
elementType: 'geometry.fill',
stylers: [{color: '#E5E5E0'}], //F9E7D9
},
{
featureType: 'road.arterial',
elementType: 'geometry.stroke',
stylers: [{color: '#FFD68C'}], //F9E7D9
},
];
//
const styles = StyleSheet.create({
container: {
justifyContent: 'flex-end',
alignItems: 'center',
},
map: {
width: 400,
height: 600,
},
text: {
fontSize: 14,
lineHeight: 21,
fontWeight: 'bold',
letterSpacing: 0.25,
color: 'black',
},
});
//
export default Gps;
Cliquez sur le bouton pour copier le codee dans le presse papier
const Compteur = () => {
const[ interval, setInter] = useState(5000);
const [count, setCount] = useState(0);
useEffect(
() => {
// paramètre obligatoire : le code de la fonction callback (l'effet à exécuter)
const ref = setInterval(
() => {
setCount((count) => count + 1);
}, interval);
console.log("Réf : " + ref + " compteur : " + count);
return (
// fonction de nettoyage pour éviter la saturation mémoire
() => {
clearInterval(ref);
console.log("fin execution compteur, clear : " + ref);
}
)
},
// paramètre optionnel : le tableau de dépendance
[count] );
console.log(" le rendu n° : " + count);
return (
<View >
<Text >
Compteur: {count}
</Text>
</View>
)
};

Exemple 2 : un compteur

Cet exemple montre comme il est fondamental de bien utiliser le tableau de dépendance.

Dans la 1ère situation, celui-ci est vide, l'effet n'est exécuté qu'une seule fois.

Dans la 2ème situation, celui-ci contient l'état count, l'effet est réexécuté à chaque modification de "count".

Cliquer pour agrandir l'image

L'image ci-dessus illustre l'exécution de la fonction-composant "compteur" lorsque le tableau de dépendance du hook "useEffect" est vide.

On y observe que la fonction callback (effet de useEffect) n'est exécuté qu'une seule fois et cela, après le 1er rendu comme le montre l'affichage dans la console de "log : rendu n° : 0".

Cette fonction exécute alors la fonction "setInterval" ( https://developer.mozilla.org/en-US/docs/Web/API/setInterval) (affichage dans la console de "log : ref 16 compteur n° : 0 "qui actionne, après 5000ms puis toutes les 5000ms suivantes, le setter "setcount" (incrémentation du compteur). Cette modification de l'état "count" produira donc, toutes les 5s, un nouveau rendu comme l'indique l'affichage dans la console des log "rendu n° .." mais sans réexécution de l'effet.

Ce comportement est observé lorsque le paramètre optionnel de dépendance est un tableau vide.

L'affichage dans la console du log "fin execution compteur, clear : 16" apparait lorsque l'on quitte la page.

Lorsque vous ajoutez "count" dans le tableau de dépendances, vous obtenez le comportement l'illustré ci-après car cette fois, useEffect est appelé lors du 1er rendu puis à chaque modification de l'état "count". c'est ce qu'indique les affichages successifs dans la console de :

"le rendu n° : 0
Réf : 43 compteur : 0
le rendu n° : 1
fin execution compteur, clear : 43
Réf : 46 compteur : 1
le rendu n° : 2
fin execution compteur, clear : 46
..."

Exemple 3 : gestion du rendu initial

Dans le code présenté ci-dessous, l'api "fetch" (voir le billet : l'API Fetch) est utilisée pour récupérer la fiche d'un "pokémon" sur le site "https://tyradex.vercel.app/api/v1/pokemon/" comme l'indique la fonction "fetchPokemonDetails" de ce code.

Comme le montre l'extrait ci-dessous, cette fonction est le "callback" du hook "useEffect" dont l'argument optionnel de dépendance est ici, un tableau vide.

useEffect(() => {
fetchDataFromURL();
},[]);

L'effet ne sera donc exécuté qu'une seule fois, après le rendu initial.

La fonction composant "details" réalisée par ce code affiche des informations (images, noms, taille...) à propos d'un "pokemon". Ces informations sont extraites de la variable d'état "data" comme l'indique, par exemple, cette ligne du code :

<Text style={styles.text}>Nom: {data.name.fr}</Text>

Or, la variable d'état "data" n'est mise à jour qu'après le rendu initial, l'expression "data.name.fr" n'est donc pas encore évaluable. Pour éviter de "planter" le composant, le rendu doit être conditionné par exemple, grâce à l'expression  :

Object.keys(data).length > 0 ? ( //rendu si vrai ) : ( //chargement en cours, rendu si faux);

Lors du rendu initial Object.keys(data).length vaut 0 et le "rendu si faux" est affiché. Lorsque l'état "data" est mis à jour dans la promesse, un nouveau rendu est lancé (effet de useState). Dans celui-ci Object.keys(data).length est positif et le rendu "si vrai" est affiché.

Cliquez sur le bouton pour copier le code dans le presse papier
    
import React, {useState, useEffect} from 'react';
import {Alert, View, Text, Image, StyleSheet, ActivityIndicator} from 'react-native';
//
const Details = () => {
const [data, setData] = useState([]);
const url = "https://tyradex.vercel.app/api/v1/pokemon/160";
//
useEffect(() => {
fetchDataFromURL();
},[]);
//
const fetchDataFromURL = () => {
fetch(url)
.then(res => res.json())
.then(resJson => setData(resJson))
.catch( error => {
console.log(error);
alert("erreur : "+error);
};
};
//
return Object.keys(data).length > 0 ? (
<View style= {{ alignItems:"center"}}>
<View >
<Image
style={styles.image}
source={{
uri: `${data.sprites.regular}`
}}
/>
<Image
style={styles.image}
source={{
uri: `${data.sprites.shiny}`
}}
/>
</View>
<View>
<Text style={styles.text}>Nom: {data.name.fr}</Text>
<Text style={styles.text}>Name: {data.name.en}</Text>
<Text style={styles.text}>Taille: {data.height}</Text>
<Text style={styles.text}>Poids: {data.weight}</Text>
<Text style={styles.text}>Type: {data.types[0].name}</Text>
</View>
</View>
) : (
<View style={styles.indicator}>
<ActivityIndicator size="large" color="red" />
<Text tyle={styles.text}> Chargement en cours </Text>
</View>
);
};
//
export default Details;
//
const styles = StyleSheet.create({
image: {
width: 200,
height: 200,
},
text: {
fontSize: 22,
marginBottom: 15,
color: 'black',
fontWeight: 'bold',
},
indicator: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});

Book navigation