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 :
14 Mar 2024

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 code 
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',
},
});