5 novembre 2018
Plongée dans les Hooks React 🦑
9 minutes de lecture
L'API de React est loin d'être figée et ne cesse d'évoluer. En témoigne l'une des dernières nouveautés proposée par Sophie Alpert et Dan Abramov lors de la ReactConf 2018. : les hooks.
Je vous propose aujourd'hui d'effectuer une petite plongée à la découverte de ces mystérieuses créatures. Suivez le guide ! 🐟
Les Hooks
Découverte de la faune
🗺 Localisation : React 16.7.0
🐡 Nombre de spécimens : 10
L'API des hooks est fonctionnelle mais reste en phase de proposition : placée dans l'agora communautaire, celle-ci peut encore évoluer d'ici sa publication dans la version 16.7 (dont une version alpha est déjà disponible). Les hooks introduisent une nouvelle manière d'utiliser les composants fonctionnels en leur donnant la capacité d'avoir un state et bénéficier de fonctionnalités jusqu'ici réservées aux classes. Les hooks permettent maintenant de :
- Partager de la logique stateful entre composants ;
- Réduire le boilerplate induit par les composants de classe et leurs méthodes de cycle de vie ;
- Limiter l'utilisation des classes grâce à une approche fonctionnelle.
Ils sont au nombre de 10 :
import {
// Basic Hooks 🐟
useState,
useEffect,
useContext,
// Additional Hooks 🦑
useReducer,
useCallback,
useMemo,
useRef,
useImperativeMethods,
useMutationEffect,
useLayoutEffect,
} from 'react'
// 🐚 🦀
Les 3 premiers hooks sont les plus courants : ils sont le ciment de notre logique applicative, les autres hooks sont semblable à des helpers et s'avèrent aussi très utiles.
Règles de sécurité
Avant de plonger quelques règles de sécurité. Les hooks sont d'une grande utilité, mais leur utilisation implique de respecter deux règles :
- Ne pas les utiliser autre part que dans des fonctions React ;
- Ne pas les utiliser dans des boucles, conditions et fonctions imbriquées.
Pour nous faciliter le respect de ces règles un plugin ESLint est disponible.
Matériel de plongée
Pour utiliser les hooks, il vous faudra installer la version alpha de React 16.7 (ou bien directement sur cette CodeSandbox):
yarn add react@next react-dom@next
Enfilez votre combinaison, chargez vos bouteilles d'oxygène, nous plongeons dès maintenant dans les profondeurs des hooks React ! ⚓️🦑
Hook useState
📒 Documentation officielle
Bonne nouvelle : les classes n'ont plus le monopole du state ! Certainement le hook le plus intéressant car il permet dorénavant de créer un composant fonctionnel stateful :
import React, { useState } from 'react'
function App() {
const [name, setName] = useState('Baptiste')
const handleChangeName = (e) => setName(e.target.value)
return (
<form>
<input value={name} onChange={handleChangeName} />
</form>
)
}
Le hook prend en paramètre la valeur initiale (ici "Baptiste") et retourne une paire de deux valeurs :
- la valeur courante (name) ;
- la fonction pour modifier cette valeur (setName).
La syntaxe utilisée ici const [name, setName]
est appelée array destructuring et permet de récupérer directement les valeurs sans passer par des index (elle donne aussi l'avantage de pouvoir nommer ses variables), et est donc équivalent à ce code :
const nameState = useState('Baptiste')
const name = nameState[0]
const setName = nameState[1]
Vous pouvez bien sûr utiliser autant de hooks useState que vous le désirez :
import React, { useState } from 'react'
function App() {
const [name, setName] = useState('Baptiste')
const [email, setEmail] = useState(null)
const handleChangeName = (e) => setName(e.target.value)
const handleChangeEmail = (e) => setEmail(e.target.value)
return (
<form>
<input value={name} onChange={handleChangeName} />
<input value={email} onChange={handleChangeEmail} />
</form>
)
}
À noter que, tout comme la fonction this.setState
, vous pouvez utiliser une fonction pour faire une mise à jour qui dépend de la valeur précédente (et ainsi éviter des effets de bord) :
const [step, setStep] = useState(0)
const handleIncrement = (e) => setStep((step) => step + 1)
Nous pouvons donc maintenant créer des composants fonctionnels stateful, mais ceux-ci sont pour l'instant limités : nous n'avons pas accès aux méthodes de cycle de vie.
Hook useEffect
📒 Documentation officielle
Ce hook permet d’ajouter notre logique comme nous le faisions avec les méthodes componentDidMount, componentDidUpdate et componentWillUnmount.
Souscrire à un évenement
Le hook est exécuté après chaque render du composant (premier compris) et permet donc de charger des données, interagir avec le DOM, etc… Vous pouvez souscrire à des événements, par exemple récupérer la largeur de la fenêtre :
import React, { useEffect, useState } from 'react'
function App() {
const [name, setName] = useState('Baptiste')
const [email, setEmail] = useState(null)
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
const handleChangeName = (e) => setName(e.target.value)
const handleChangeEmail = (e) => setEmail(e.target.value)
// ✨ Notre hook useEffect ✨
useEffect(() => {
const handler = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handler)
})
return (
<form>
<input value={name} onChange={handleChangeName} />
<input value={email} onChange={handleChangeEmail} />
<div>Largeur fenêtre : {windowWidth}</div>
</form>
)
}
Clean up
Il reste un problème avec l'implémentation ci-dessus : à chaque mise à jour, notre composant va ré-écouter notre événement, ainsi le handler sera appelé autant de fois que le composant s’est rendu !
Heureusement, il est possible de retourner une fonction dans votre hook : celle-ci sera exécutée avant chaque nouveau effect (et lorsque le composant est supprimé du DOM). Cela vous permet de supprimer votre listener :
useEffect(() => {
const handler = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handler)
// Clean up
return function cleanup() {
window.removeEventListener('resize', handler)
}
})
Gérer plus finement vos effects
Dernier détail qui a son importance : vous pouvez passer un tableau de variable en deuxième argument, le hook ne sera alors appelé que si l’un de ses arguments change. Cela peut être très utile pour appeler une API si une props change afin de rafraîchir les données :
function App({ username }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`https://api.github.com/users/${username}`)
.then((response) => response.json())
.then((data) => {
setUser(data)
})
}, [username])
return <section>{user && user.email}</section>
}
Ici, le hook sera appelé uniquement lorsque la props username passée à notre composant changera.
Vous pouvez paramétrer votre hook afin qu'il s'exécute une seule fois (semblable à un componentDidMount donc) en lui passant un tableau vide :
useEffect(() => {
// code
}, [])
Enfin, tout comme le hook useState, vous pouvez l'appeler autant de fois que vous voulez.
Créer ses propres hooks
L'une des grandes forces des hooks est de pouvoir les extraire dans ses propres fonctions et créer ses propres hooks. Ainsi nous pouvons reprendre toute la logique ajoutée plus haut et la déplacer dans une simple fonction (qui par convention doit commencer par use) :
import React, { useState } from 'react'
function App() {
const windowWidth = useWindowWidth()
return <div>Largeur : {windowWidth}</div>
}
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
useEffect(() => {
const handler = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handler)
})
return windowWidth
}
Il devient alors facile d'utiliser cette même logique dans d'autres composants : cela était déjà possible avec des patterns comme les render props, mais les hooks React apportent une API plus simple et concise.
Cette nouvelle possibilité, a très vite inspiré la communauté, qui n'a pas attendu pour sortir des collections de hooks. Il en existe déjà des dizaines :
Les librairies react-use, react-powerhooks, react-hanger, rehooks, the-platform proposent toutes des collections de hooks, prêtes à l'emploi. Certains sites comme hooks.guide centralise l'ensemble des hooks.
La communauté n'a donc pas attendu la version 16.7 pour s'y intéresser !
Les autres hooks
Nous avons vu les deux hooks les plus importants, mais la nouvelle API recèle d'autres hooks très utiles. Présentation.
Hook useContext
📒 Documentation officielle
La version 16.6 de React apportait déjà une simplification de l’API pour les classes, ce hook facilite encore plus l'utilisation du context au sein d'un composant fonctionnel :
const context = useContext(Context)
A chaque fois que le provider sera modifié, le hook provoquera un nouveau rendu.
Hook useCallback
📒 Documentation officielle
Vous avez du lire des dizaines de fois : inliner des fonctions dans vos composants peut provoquer des problèmes de performance. En effet, lors du rendu de votre composant une nouvelle fonction sera créée à chaque fois, empêchant de bloquer les rendus inutiles. En utilisant le hook useCallback plus de soucis :
const handleOnSubmit = useCallback(() => {
// code
}, [a, b])
Ce hook permet de toujours passer la même instance de votre fonction si les paramètres ne changent pas. Plus besoin donc de créer une classe pour sortir les fonctions de votre méthode render !
Hook useMemo
📒 Documentation officielle
Ce hook permet de mémoiser une fonction, c’est à dire créer une sorte de cache afin de l’exécuter uniquement si les arguments ont changés. Si la fonction a déjà été exécutée avec les arguments donnés, elle retournera directement la valeur sauvegardée. Cela peut améliorer les performances de vos composants en cas de fonctions gourmandes.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
Hook useRef
📒 Documentation officielle
Ce hook permet de créer une référence dans votre composant fonctionnel, pour par exemple focus un champs :
function App() {
const inputElement = useRef(null)
useEffect(() => {
inputElement.current.focus()
}, [])
return (
<div>
<input ref={inputElement} type="text" />
</div>
)
}
Hook useReducer
📒 Documentation officielle
Permet d’embarquer un mini Redux dans vos composants, ce hook utilise une API semblable à Redux en retournant un state et une fonction de dispatch pour modifier votre store via un reducer. Cela peut s’avérer très pratique pour gérer des states complexes ou des machines à états.
A noter que ce hook accepte un troisième argument permettant de définir une action d’initialisation (jouée lors du premier rendu).
Voici une implémentation minimaliste de Redux avec ce hook et l'utilisation du contexte (disponible aussi sur cette CodeSandbox) :
// Redux
const StateContext = React.createContext(null)
// Nous utilisons un contexte séparé pour le dispatch car cette fonction n'est jamais modifié, nous évitons ainsi des rendus si un composant ne nécessite pas le state.
const DispatchContext = React.createContext(null)
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'reset':
return { count: action.payload }
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
}
}
// Composant lié au state et a la fonction de dispatch
function DeepDecrement() {
const state = useContext(StateContext)
const dispatch = useContext(DispatchContext)
return (
<button onClick={() => state.count > 0 && dispatch({ type: 'decrement' })}>Decrement</button>
)
}
// Composant uniquement lié au dispatch
function DeepIncrement() {
const dispatch = useContext(DispatchContext)
return <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
}
// Composant d'application avec initialisation du reducer et création des providers pour le contexte
function App() {
const initialCount = 0
const [state, dispatch] = useReducer(reducer, initialState, {
type: 'reset',
payload: initialCount,
})
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<div className="App">
<p>{state.count}</p>
<DeepDecrement />
<DeepIncrement />
</div>
</DispatchContext.Provider>
</StateContext.Provider>
)
}
Fin de la plongée !
Remontons à la surface pour reprendre un peu d'air ! Nous le rappelons : cette API est toujours en phase de proposition. Il n'est pas recommandé de ré-écrire toutes vos applications avec ce pattern pour l'instant, mais il est intéressant de commencer à l'expérimenter sur vos nouveaux composants !
Comme le précise Dan Abramov dans son article, la communauté s'est rapidement emparé des hooks, mais même pour leurs créateurs il reste des zones d'ombre :
The content industry around React is both a blessing and a curse. It’s amazing that many talented people create great articles and examples. But it creates unnecessary hype around an early stage proposal and makes people feel lost and confused. Spread love, not hype.
— Dan Abramov (@dan_abramov) October 29, 2018
💜