Qu'est-ce qu'un hook custom ?
Les Hooks sont une nouveauté de React 16.8. Ils permettent de bénéficier d’un état local et d’autres fonctionnalités de React sans avoir à écrire de classes.
Un hook custom nous permet de composer des fonctionnalités complexes en React, en les encapsulant dans des fonctions. Il est ainsi possible de réutiliser de la logique autre que d'affichage entre plusieurs composants React.
Cet article part du principe que vous connaissez les hooks useState et useEffect de React.
L'objet Audio en Javascript
Pour lire de la musique, on peut utiliser l'objet Audio en Javascript.
Imaginons par exemple, que l'on récupère une URL vers un fichier audio, par exemple depuis l'api de Spotify, Deezer...
Il s'emploie de la façon suivante :
const url = '<http://monsite.fr/monfichieraudio.mp3>'; const audioObj = new Audio(url);
On obtient alors un objet audio, qui contient des méthodes permettant de contrôler la lecture.
audioObj.play(); audioObj.pause();
Il est également possible de s'abonner à des événements, par exemple lorsque la lecture se termine :
audio.addEventListener('ended', () => { console.log('la lecture est terminée'); });
Notre composant React
Imaginons maintenant que l'on souhaite créer un bouton en React, qui contrôle la lecture de la musique.
Le JSX ressemblerait à ceci :
<Button onClick={togglePlayBack} icon labelPosition="left"> <Icon name={playing ? 'pause' : 'play'} /> {playing ? 'Pause' : 'Play'} </Button>
On appellera lors du clic sur le bouton une fonction togglePlayBack qui lancera ou mettra en pause la musique. On aura également un booléen playing qui permettra de savoir si la musique est en cours de lecture ou non.
Nous pouvons donc commencer par déclarer ce state playing, ainsi que la fonction togglePlayBack qui inverse sa valeur :
const [playing, setPlaying] = useState(false); const togglePlayBack = () => setPlaying(!playing);
Nous allons également créer un objet Audio, pour contrôler la lecture. Cependant, nous ne voulons le créer qu'une seule fois, et pas à chaque render ! Sinon, à chaque appui sur play / pause cet objet serait recréé et la musique se lancerait plusieurs fois.
Nous pouvons donc créer cet objet audio et le stocker une bonne fois pour toutes, grâce au hook useState :
const [audioObj] = useState(new Audio(url));
Enfin, nous souhaitons que, lorsque la lecture se termine, notre état playing revienne à false . Nous allons donc créer un nouvel écouteur d'évènement comme vu plus haut. Afin que l'écouteur soit créé au chargement du composant, nous allons employer le hook useEffect avec un tableau de dépendances vides, soit l'équivalent d'un componentDidMount :
useEffect(() => { audio.addEventListener('ended', () => setPlaying(false)); }, []);
Problème : il ne faudrait pas que notre écouteur d'évènement reste en place lorsque le composant est détruit. Nous pouvons donc utiliser la fonction de nettoyage du useEffect pour détruire notre écouteur :
useEffect(() => { audio.addEventListener('ended', () => setPlaying(false)); // la fonction ci dessous sera appelée à la destruction du composant return () => { audio.removeEventListener('ended', () => setPlaying(false)); }; }, []); // tableau de dépendances vides : appelé une fois au chargement du composant
Enfin, lorsque notre composant change la valeur du state playing, nous voulons lire ou mettre en pause la musique. Nous pouvons employer le hook useEffect avec comme dépendance la variable playing : la fonction sera alors appelée lorsque la valeur de playing change.
// fonction appelée lorsque la valeur de "playing" change useEffect(() => { // si playing vaut true, on lance la lecture if (playing) { audio.play(); } // si playing vaut false, on arrête la lecture else { audio.pause(); } }, [playing]); // le tableau de dépendances indique a quel moment appeler notre effet // ici, l'effet est appelé lorsque playing change.
Le code final de notre composant ressemblerait donc à ceci :
const AudioPlayer = ({ url }) => { const [audio] = useState(new Audio(url)); const [playing, setPlaying] = useState(false); const togglePlayBack = () => setPlaying(!playing); useEffect(() => { if (playing) { audio.play(); } else { audio.pause(); } }, [playing]); useEffect(() => { audio.addEventListener('ended', () => setPlaying(false)); return () => { audio.removeEventListener('ended', () => setPlaying(false)); }; }, []); return ( <Button onClick={togglePlayBack} icon labelPosition="left"> <Icon name={playing ? 'pause' : 'play'} /> {playing ? 'Pause' : 'Play'} </Button> ); };
La création d'un hook custom
Enfin, nous pouvons extraire tout ce code, et en faire un hook custom useAudio :
// un hook custom pour gérer l'audio const useAudio = (url) => { const [audio] = useState(new Audio(url)); const [playing, setPlaying] = useState(false); const togglePlayback = () => setPlaying(!playing); useEffect(() => { if (playing) { audio.play(); } else { audio.pause(); } }, [playing]); useEffect(() => { audio.addEventListener('ended', () => setPlaying(false)); return () => { audio.removeEventListener('ended', () => setPlaying(false)); }; }, []); return [playing, togglePlayback]; }; const AudioPlayer = ({ url }) => { const [playing, togglePlayback] = useAudio(url); return ( <Button onClick={togglePlayback} icon labelPosition="left"> <Icon name={playing ? 'pause' : 'play'} /> {playing ? 'Pause' : 'Play'} </Button> ); };
Nous retrouvons tout le code vu au dessus, que nous avons encapsulé dans une fonction.
Cette fonction renvoie les deux choses dont nous avons besoin dans le composant : la valeur du state playing, et une fonction pour modifier le state togglePlayback.
Pour cette étape, il est important de respecter une convention de React : notre hook doit commencer par use.
Conclusion
L'emploi de hook customs nous permet d'encapsuler efficacement de la logique que l'on souhaite réutiliser entre nos composants. Comme nous l'avons vu, nos hooks custom peuvent sans problème utiliser les hooks de base de React que sont useState et useEffect. Il s'agit donc d'un outil très intéressant pour mieux découper notre code.