Cette nouvelle fonctionnalité vient d'être présentée lors de la React Conf 2018.
Disponible dans React 16.7.0 en alpha, une RFC est d'ailleurs ouverte afin de recueillir l'avis de la communauté.
La question que se pose tout développeur avant de définir un composant est : une classe ou une fonction ?
La façon la plus simple d'écrire un composant est la fonction :
import React from "react";
function Form(props) {
return (
<div>
<input type="text" value="name" name="putaindecode" />
</div>
);
}
Mais on peut également utiliser les classes ES6 pour écrire la même chose :
import React from "react";
class Form extends React.Component {
render() {
return (
<div>
<input type="text" value="name" name="putaindecode" />
</div>
);
}
}
L'avantage des classes est la possibilité d'utiliser les fonctionnalités de React comme par exemple le lifecycle ou encore le state.
C'est très précisement à ce niveau qu'interviennent les hooks. Il est dorénavant
possible (enfin, ça le sera bientôt) d'avoir accès à ces deux concepts (mais pas
que, en gros toutes les fonctionnalités de React comme les ref
par exemple) dans
les fonctions.
Petit bémol : comme le précise l'équipe de React, ne vous jetez pas tout de suite dans le refactorisation de vos classes. L'utilisation des hooks dans les fonctions ne signe pas pour autant la fin des classes.
Il faut cependant avouer qu'on est pas loin non plus d'une utilisation moins
intensives des classes surtout si on ajoute en plus
React.memo
qui est l'intégration des PureComponent/shouldComponentUpdate
dans les
fonctions.
state
et useState
Partons de notre exemple précédent en y rajoutant l'utilisation d'un state :
import React from "react";
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "putaindecode",
};
this.handleNameChange = this.handleNameChange.bind(this);
}
handleNameChange(e) {
this.setState({
name: e.target.value,
});
}
render() {
return (
<div>
<label htmlFor="inputName">Name</label>
<input
id="inputName"
type="text"
value={this.state.name}
name="firstname"
onChange={this.handleNameChange}
/>
</div>
);
}
}
Nous venons d'écrire un composant très simple qui met à jour un élément
<input />
. On a dû, pour cela, définir dans le constructor une valeur par défaut
dans notre objet state
, définir une fonction pour gérer l'événement onChange
sur notre <input />
sans oublier de bind notre fonction pour le scope de
this
.
Pfiou, les classes c'est bien, mais ça fait un peu de boilerplate quand même.
Transformons cette classe en fonction et voyons le changement opéré grâce à nos fameux hooks.
import React, { useState } from "react";
function Form(props) {
const [name, setName] = useState("putaindecode");
function handleNameChange(e) {
setName(e.target.value);
}
return (
<div>
<label htmlFor="inputName">Name</label>
<input
id="inputName"
type="text"
value={name}
name="firstname"
onChange={handleNameChange}
/>
</div>
);
}
Attardons-nous un peu plus sur ce useState
.
C'est le premier hook introduit par React qui nous permet d'utiliser le state dans les fonctions React.
Il prend un argument qui est la valeur par défaut de notre state.
// fonction
useState("putaindecode")
// classe
constructor(props) {
this.state = {
name: "putaindecode"
}
}
Il retourne un array comportant 2 éléments : le state courant (name
) ainsi que
la fonction qui va permettre le changement de notre state (setName
).
Et voilà, rien de plus, rien de moins. Notre fonction stateless
devient grâce
au hook useState
un composant avec un state.
Cette fonction React fait au final exactement la même chose que notre classe précédente.
useEffect
Ajoutons un soupçon de lifecycle dans notre exemple.
Mettons à jour notre classe en utilisant les méthodes componentDidMount()
et
componentWillUnmount()
.
import React from "react";
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "putaindecode",
width: window.innerWidth,
};
this.handleNameChange = this.handleNameChange.bind(this);
this.handleResize = this.handleResize.bind(this);
}
handleNameChange(e) {
this.setState({
name: e.target.value,
});
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
handleResize() {
this.setState({
width: window.innerWidth,
});
}
render() {
return (
<div>
<label htmlFor="inputName">Name</label>
<input
id="inputName"
type="text"
value={this.state.name}
name="firstname"
onChange={this.handleNameChange}
/>
<div>{this.state.width}</div>
</div>
);
}
}
On veut donc surveiller le redimensionnement de notre navigateur et afficher sa résolution.
Les bonnes pratiques nous conseillent de souscrire à notre événements dans la
methode componentDidMount()
et pour éviter les fuites mémoires inutiles de
s'en désabonner dans la méthode componentWillUnmount()
.
Comme précédemment on n'oublie pas d'initialiser la valeur dans notre state et
de bind notre fonction handleResize()
.
Même exercice, transformons tout cela pour l'appliquer à notre function.
import React, { useState, useEffect } from "react";
function Form(props) {
const [name, setName] = useState("putaindecode");
function handleNameChange(e) {
setName(e.target.value);
}
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
return (
<div>
<label htmlFor="inputName">Name</label>
<input
id="inputName"
type="text"
value={name}
name="firstname"
onChange={handleNameChange}
/>
<div>{this.state.width}</div>
</div>
);
}
Pour être conforme à notre classe nous avons de nouveau utilisé le hook
useState
pour garder un state de notre valeur width
et déterminer son setter
(setWidth
).
Pour pouvoir jouer avec le lifecyle nous ajoutons cette fois-ci
useEffect. Ce hook suit la même
logique que componentDidMount
, componentDidUpdate
et componentWillUnmount
.
Il va donc s'exécuter :
Petit précision sur le return
de notre hook : il faut considérer cela comme un
mécanisme (optionnelle) de cleanup.
Concrètement, dans notre cas, c'est ici qu'on va pouvoir de se désabonner de notre événement.
Grace à ces deux hooks, nous nous avons donc accès aux concepts de state
et du
lifecycle
dans une fonction.
React a également mis à disposition un autre hook
useContext qui
permet de souscrire à
React context
en évitant l'imbrication des Consumer
et Provider
.
Tous ces hooks sont, bien entendu, disponibles dans la version 16.7.0
alpha de
React.
Ils en existent d'autre qui sont listés ici : https://reactjs.org/docs/hooks-reference.html#additional-hooks
Vous l'avez sans doute remarqué mais tous ces hooks commencent par use
. Ce
préfixe est primordial pour considérer votre hook comme un hook.
De plus un hook ne doit ni se trouver dans une condition ni dans une boucle et ne doit s'appeler que dans une fonction React.
Il existe un plugin ESLint
eslint-plugin-react-hooks
qui permet d'être sûr que ces régles soient bien respectées.
Naturellement il est tout à fait possible de créer soi-même son propre hook, une orga sur GitHub existe ayant pour objectif de recenser tous ces custom hooks.
Revenons à notre exemple pour voir si on ne pourrait pas le modifier un peu pour tenter de créer un hook.
import React, { useState, useEffect } from "react"
function Form(props) {
const [name, setName] = useState("putaindecode")
const width = useWidth()
function handleNameChange(e) {
setName(e.target.value)
}
return (
<div>
<label htmlFor="inputName">Name</label>
<input
id="inputName"
type="text"
value={name}
name="firstname"
onChange={handleNameChange}
/>
<div>
{this.state.width}
</div>
</div>
)
}
function useWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
})
return width
}
En externalisant notre partie de code sur le calcul de width
, on vient tout
simplement de créer un hook.
Les hooks présentés dans cet article permettent donc de s'abstraire des classes.
Elles ouvrent la voix à un peu plus de généricité et à des tests orientés sur le comportement plus que sur l'implémentation.
L'équipe de React a très bien documenté tout cela. Tous ces exemples proviennent de l'excellente conférence de Dan Abramov React Today and Tomorrow and 90% Cleaner React que je vous invite vivement à regarder.
Comme precisé en début d'article, les hooks sont une proposition faite par l'équipe de React. Tout est disponible dans une version alpha, ce qui implique une possibilité dans le changement de l'API voir même du noms des hooks.