Loupe

Les hooks dans React - Retour d'experience

Il y a quelques temps, je vous avais fait un article qui introduisait une des récentes nouveautés importantes de React, à savoir : les Hooks. Je vous invite à aller y jeter un coup d’œil si vous ne voulez pas être trop perdus dans la lecture de cet article.

Le hasard fait qu’il se trouve que juste après avoir commencé à écrire ce précédent article, je me suis mis à devoir utiliser les hooks lors d’un projet client. Du coup : Retour expérience !

Alors les hooks dans la vraie vie, ça donne quoi ?

Qui dit hooks, dit Functional Component et qui dit Functional Component dit pleins d’avantages par rapport aux Class Component ! :

  • Pas de class et donc pas de keyword class, pas d'instanciation et donc on ne s’embête pas avec de la logique de class js.
  • Pas de constructor.
  • Pas de this (ça peut faire éviter quelques bugs)
  • Beaucoup plus simple à lire et donc à débugger. (On évite le code verbeux des class)
  • Une manipulation beaucoup plus simple et performante des événements et changements de props.

C’est sur ces deux derniers points que j’aimerais revenir et c’est à mon sens là où les hooks excellent réellement et où on ressent véritablement une vrai plus-value en pratique par rapport aux Class Component.

La simplicité

Si vous doutiez de la simplicité des Functional Components par rapport aux Class Component , voici simplement le même code dans les deux versions.

Nous souhaitons afficher un composant qui log "début du jeu" quand le composant mount, "fin du jeu" quand le composant unmount et "pong" quand la props ping vaut true

Version class Component :

class ClassComponentPong extends React.Component {
    constructor(props) {
	    super(props);
    }
	
	componentDidMount() {
		console.log("début du jeu");
	}
	
	componentWillUnmount() {
		console.log("fin du jeu");
	}

	componentDidUpdate(prevProps) {
		if (prevProps.ping !== this.props.ping &&
		    prevProps.ping) {
			console.log("pong");
		}
	}

	render() {
		return <div />
	}
}

version Functional Component :

const FunctionalComponentPong = ({ ping }) => {
    useEffect(() => {
	    console.log("début du jeu");

		return () => console.log("fin du jeu");
    }, []);

	useEffect(() => {
		if (ping) {
			console.log("pong");
		}
	}, [ping]);
	
	return (
		<div />
	);
}

Sans parler de state management, qui n’est pas présent dans cet exemple, on peut remarquer plusieurs choses :

  • On profite de la destructuration ES6 pour directement récupérer les variables de props qui nous intéressent. Ça rend beaucoup plus lisible ce qu’on prend en paramètre et on ne s’embête pas à utiliser en permanence props.leNomDeMaVariable. (Idéalement on couplera ça avec une interface Typescript ou des propTypes pour encore plus de lisibilité et de contrôle)

  • Comme dit plus haut, pas de this, on utilise la variable directement.

  • useEffect permet d’agréger tous les lifecycle hooks de React. Dans notre cas précis, on en utilise un pour gérer la construction / destruction de composant et un autre pour gérer spécifiquement l’action qui va être trigger par le changement de variable. On ne s’embête pas à savoir quel lifecycle hooks fait quoi ou a quel moment. Les useEffect respectent tout le temps le même schéma, à savoir :

    • Ils sont exécutés quand le composant mount et après chaque nouveau rendu.
    • Les exécutions non nécessaires peuvent-être contrôlées en passant un tableau de variables en second paramètre qui va autoriser exécution de la fonction lorsque l’une de ces variables est modifiée.
    • Entre deux exécutions et quand le composant unmount, le callback passé en return est exécuté.

    C’est un outil assez puissant et flexible qui permet ensuite au développeur de les utiliser comme bon lui semble en fonction des cas. Cependant, attention à bien maîtriser les valeurs passées dans le tableau de dépendance en second paramètre du useEffect. Si une variable ou fonction passée en dépendance est modifiée dans des cas non prévus ou que vous ne contrôlez pas, cela peut mener à des bugs durs à reproduire et complexes à débugger ou tout simplement des boucles infinies. (Merci à Alexandre pour ce point important :) ) 

Au final, dans le cadre d’un projet concret, on se retrouve vite à avoir beaucoup moins de code et du code plus lisible qui plus est. Et ça, c’est vraiment un bon point ! ;)

Le flow de changements de variables et la performance

Maintenant je vais vous présenter un exemple concret de workflow que j’ai dû réaliser pour un client.

J’avais pour mission de faire un widget qui consomme les API de Microsoft Planner, le besoin était simple : afficher une liste de tâches.
Sans rentrer dans les détails de MS Planner, ce dernier étant un produit MS Office, il faut laisser à l’utilisateur la possibilité de pouvoir choisir quel groupe office il souhaite utiliser. Suite à cela, l’utilisateur pourra choisir un plan associé à ce groupe (un plan est en quelque sorte le répertoire des tâches) car un groupe office peut avoir plusieurs plans. Ces plans ont eux-mêmes à leur tour des buckets (groupe de tâches) que l’on doit laisser à l’utilisateur le soin de pouvoir sélectionner. Et finalement, une fois le bucket sélectionné, on peut récupérer les tâches associées à ce bucket.

Tel était le workflow demandé, un simple emboîtement de sélections où chacune d’elles débloquait la sélection suivante jusqu’à avoir tous les renseignements dont nous avions besoin pour récupérer les tâches.

L’avantage d’un hook comme useEffect est qu’il va nous permettre d’isoler chaque partie indépendante du code. Ces parties vont ensuite se trigger entre elles sans que l’on ait à les appeler explicitement.

Récupération des groupes Office :

useEffect(() => {
    if (!user) {
	    setSelectedGroup(null);
	} else {
	    api.getOfficeGroups(user)
		    .then((res) => setOfficeGroups(res));
	}	        
}, [user]); // Est executé quand l'utilisateur actuel change.

Ensuite, comme indiqué dans le workflow, une fois qu’un groupe est sélectionné par l’utilisateur, on affiche les plans de ce groupe. Et pour cela, même principe :

useEffect(() => {
    if (!selectedGroup) {
	    setSelectedPlan(null);
    } else {		
	    api.getPlansByOfficeGroup(selectedGroup)
		    .then((res) => setPlans(res));
    }
}, [selectedGroup]);

La récupération des buckets suit le même schéma :

useEffect(() => {
    if (!selectedPlan) {
	    setSelectedBucket(null);
    } else {
	    api.getBucketsByPlan(selectedPlan)
		    .then((res) => setBuckets(res));
    }
}, [selectedPlan]);

Et finalement, la récupération des tâches :
useEffect(() => {
    if (selectedBucket) {
	    api.getTasksByBucket(selectedBucket)
		    .then((res) => setTasks(res));
    }
}, [selectedBucket]);

(Veuillez me pardonner pour la redondance de cet exemple ;-) )

Une fois cela fait, il ne nous restera plus qu’à afficher une série de <select /> où chacun se chargera d’afficher l’un des jeux de données et d’avoir un onChange permettant de set la bonne valeur. Une fois la valeur mise à jour, elle déclenchera le useEffect associé. On peut même imaginer qu’à chaque récupération de jeu de données, on présélectionne automatiquement la première valeur. Cela permettra d’avoir une cascade d’appel automatique qui aboutirait à chaque fois à l’affichage de tâches d’un bucket spécifique. Simple et efficace !

On peut voir dans cet exemple que les useEffects peuvent servir de listeners scopés sur une ou plusieurs variables et ainsi déclencher de manière autonome différentes actions. Cela nous permet complètement d’éviter des appels synchrones où chaque fonction appellerait explicitement la suivante et où chaque changement de donnée devrait explicitement appeler sa fonction associée pour poursuivre le workflow de données.

A noter que dans notre cas, il nous était demandé de n’utiliser aucune variable de state et de tout stocker dans un store Redux, nous recevions donc nos variables via les props et leur changement était possible en dispatchant des actions Redux. Cela peut paraître complexe mais il n’en est rien ! Mise à part la partie “store Redux” qui sera externalisée et dont nous ne parlerons évidemment pas ici, le code React, lui, restera exactement le même ! Les variables de state deviendront des props mais du point de vue des useEffect, cela ne changera rien alors qu’avec un Class Component, nous aurions dû passer par un componentWillReceiveProps , comparer chaque changement de prop avec la valeur précédente et en fonction de ça, exécuter la bonne fonction… bref, dans ce genre de cas, les hooks nous facilitent clairement la vie !

Le problème de conversion

Il m’a également été demandé durant cette mission de convertir un composant Angularjs en Functional Component. J’ai naïvement cru que ce ne serait qu’une affaire de conversion simple de variables et de fonctions, enfin, juste des symboles à changer quoi… (ce qui aurait été le cas avec les Class Components). J’ai cependant juste omis un détail de la documentation :

Le setter retourné par useState ne retourne pas, lors de l’exécution, de Promises

Rappelons que c’est pourtant le cas du setState des Class Components. Les deux setters étant asynchrones

Mais du coup, ça change quoi ? Et bien… ça change tout ! (Ou en tout cas pas mal de choses).

Avant, si vous souhaitiez mettre une variable de state à jour et récupérer sa valeur immédiatement, vous aviez la possibilité de subscribe à setState en faisans comme ceci :

changeName(newName) {
    setState({ name: name })
	    .then(() => {
		    console.log(`Now my name is: ${this.state.name}`);
	    }
}

C’est, il faut le reconnaître, assez pratique notamment quand on est dans des grosses fonctions où il y a plusieurs setStates qui peuvent s’imbriquer avec pas mal de logique ; ça permet de ne pas trop avoir à se soucier que setState soit asynchrone…
Mauvais point et comme dit plus haut, le setter renvoyé par useState est asynchrone et ne renvoie pas de Promise

Alors on fait comment ? Et bien on suit les guidelines de la team React qui nous dit que les useEffects servent justement à catcher les changements de variables et que c’est suffisant. C’est suffisant certes, mais dans un cas de migration comme celui que j’ai eu à faire, on se retrouve à devoir changer complètement de pattern et à réorganiser le code. Et ça, c’est beaucoup moins sympa, surtout si la compréhension du code à traduire est partielle.

C’est à mon sens l’un des points forts et faibles des Functional Components : on se retrouve cadré dans un pattern de réalisation et c’est assez compliqué d’en sortir. Cependant, il a des avantages certains que l’on a pu voir dans cet article.

 

Voilà en quelques mots mon retour d'expérience sur les hooks et les Functional Components de react en général. Je me suis retrouvé à devoir les utiliser pendant plusieurs mois et je dois le reconnaître, les essayer, c’est les adopter. Si de plus on prend en compte que la Team React à clairement l’intention d’orienter les prochaines releases dans ce sens, on a toutes les raisons pour désormais faire des Functionnal Components, surtout qu’ils cohabitent très bien avec des Class Components! :)

C’est tout pour cet article, prochainement je vous parlerai d’une release d’une grosse librairie React qui décide d'implémenter des custom hooks afin de simplifier son utilisation et rendre la vie plus simple aux utilisateurs. A suivre donc !

Happy Coding ! :-)

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus