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
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.
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 :
les variables d'état utilisées pour mémoriser :
la position de l'utilisateur : location ;
la route qu'il suit : route.
les fonctions "callback" des hooks 'useEffect".
Elles sont en gras dans le code. Les effets (callback) sont :
la recherche de la position actuelle (donc la modification de l'état "location" du composant GPS) par la fonction "requestInitLocation();" ;
la mise à jour cette position (donc la modification de l'état "location" du composant GPS) par la fonction "locationUpdate();" ;
locationUpdate utilise par exemple "Geolocation.watchPosition()" pour suivre en temps réel le déplacement de l'utilisateur.
le "nettoyage" en arrêtant de mettre à jour de la position.
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.
//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;
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".
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é.
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',
},
});