Construire un Chat Vidéo avec React Hooks

October 09, 2019
Rédigé par
Phil Nash
Twilion

Construire un Chat Vidéo avec React Hooks

On avait déjà vu la construction d’un chat vidéo avec React dans un article précédent, mais depuis sa publication, React a sorti la version 16.8 de Hooks. Hooks nous laisse utiliser State ou n’importe quelle autre fonction React dans des composants fonctionnels au lieu de devoir coder une classe composant.

Dans ce post, nous allons construire une application vidéo chat en utilisant Twilio Video et React avec des composants fonctionnels uniquement, en utilisant les hooks useState, useCallback, useEffect et useRef.

Ce dont vous aurez besoin

Pour construire cette application de chat vidéo, vous aurez besoin :

Une fois que vous avez tout ça, nous pouvons préparer notre environnement de développement.

Pour commencer

Pour pouvoir passer directement à l’application React, vous pouvez vous lancer avec l’application que j’ai créée, React and Express starter. Téléchargez ou clonez la branche twilio de l’app starter, allez dans le nouveau répertoire et installez les dépendances :

git clone https://github.com/philnash/twilio-video-react-hooks.git
cd twilio-video-react-hooks
npm install

Copiez le fichier .env.example sur .env.

cp .env.example .env

Exécutez l’application pour vous assurer que tout fonctionne comme prévu :

npm run dev

Vous devriez voir cette page dans le browser :

La page initiale montre le logo React et un formulaire

Préparer les identifiants Twilio

Pour vous connecter à Twilio Video, vous aurez besoin de quelques identifiants. Depuis la console Twilio, copiez votre Account SID (SID de compte) et collez-le dans le fichier .env comme suit : TWILIO_ACCOUNT_SID.

Nous aurons aussi besoin d’une clé et d’un secret API - que l’on peut créer sur notre console dans les Programmable Video Tools. Il faut créer la paire de clé et ajouter le SID et Secret dans le fichier .env à la place de TWILIO_API_KEY et TWILIO_API_SECRET.

Ajouter un peu de style

Nous n’allons pas nous embêter plus que nécessaire avec CSS dans ce tutoriel, mais ajoutons-en tout de même un peu afin que le résultat ne soit pas trop moche ! Prenons donc le CSS via cette URL et remplaçons le contenu de src/App.css par celle-ci.

Nous sommes maintenant prêts à commencer !

Prévoir nos composants

Tout commencera d’abord dans notre composant App où l’on peut poser un header (en-tête) et un footer (pied-de-page) pour l’application, ainsi qu’un composant VideoChat. A l’intérieur du composant VideoChat, on souhaite montrer un composant Lobby où l’utilisateur peut entrer son prénom et le salon qu’ils veulent rejoindre. Une fois que c’est fait, on remplace le composant Lobby par Room, qui s’occupera de connecter le salon et d’afficher les participants dans la discussion video.

Et enfin, pour chaque participant dans le salon, nous rendrons un composant Participant qui s’occupera d’afficher leur média.

Coder les composants

Le composant App

En ouvrant src/App.js, on peut voir qu’il y a beaucoup de code - issu de l’exemple d’app initial - que l’on peut retirer. A savoir aussi, le composant App est un composant de classe : comme nous avons dit que l’on construirait l’application entière avec des composants fonctionnels, on ferait mieux de changer ça !

Dans les imports, enlevez Component et l’import du logo.svg. Remplaçons la classe App entière par une fonction qui renvoie la structure de de notre application. Le fichier complet devrait ressembler à ça :

import React from 'react';
import './App.css';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <p>VideoChat goes here.</p>
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

Le composant VideoChat

Ce composant va afficher un salon basé sur le nom d’utilisateur ou sur le nom du salon que l’utilisateur a entré. Créons un nouveau fichier composant src/VideoChat.js et lançons le avec le boilerplate suivant :

import React from 'react';

const VideoChat = () => {
  return <div></div> // we'll build up our response later
};

export default VideoChat;

Le composant VideoChat sera le meilleur composant pour gérer les données du chat. Nous aurons besoin d’enregistrer un username pour l’utilisateur qui rejoint la discussion, un nom pour le salon auquel ils vont se connecter et leur access token, une fois qu’il a été récupéré du serveur. On va développer un formulaire pour entrer quelques unes de ces données dans le composant suivant.

Avec React Hooks, on utilise le hook useState pour stocker ces données.

useState

La fonction useState prend un seul argument , l’état initial, et affiche un tableau qui contient l’état actuel ainsi qu’une autre fonction pour mettre à jour cet état. Nous allons déstructurer ce tableau pour qu’il nous rende deux variables distinctes comme state et setState. On utilisera setState pour tracker le nom d’utilisateur, le nom du salon, et le token à l’intérieur de notre composant.

Commençons par importer useState à partir de React et configurons les états du nom d’utilisateur, du nom de salon et du token:

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  return <div></div> // we'll build up our response later
};

Next we need two functions to handle updating the username and roomName when the user enters them in their respective input elements.

Ensuite, nous avons besoin de deux fonctions pour gérer la mise à jour de username et  de roomName une fois que l’utilisateur les entrera dans leur éléments de saisie respectifs.

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = event => {
    setUsername(event.target.value);
  };

  const handleRoomNameChange = event => {
    setRoomName(event.target.value);
  };

  return <div></div> // we'll build up our response later
};

Bien que ça fonctionne, on peut tout de même optimiser notre composant en utilisant un autre hook React ; useCallback.

useCallback

A chaque fois qu’on appelle une  fonction composant, les fonctions handleXXX sont redéfinies. Elles ont besoin de faire partie du composant parce qu’elles s’appuient sur les fonctions setUsername et setRoomName, mais elles seront pareilles à chaque fois. Le hook React useCallback nous permet de mémoïser les fonctions. C’est-à-dire que si elles sont les mêmes entre les invocations de fonctions, elles ne seront pas redéfinies.

useCallback prend deux arguments, la fonction qui doit être mémoïsée et un tableau des dépendances des fonctions. Si l’une de ces dépendances change, ça implique que la fonction mémoïsée est dépassée. La fonction est alors redéfinie et mémoïsée à nouveau.

Dans ce cas, les deux fonctions n’ont pas de dépendances donc un tableau vide sera suffisant (les fonctions setState du hook useState sont réputées pour être constantes au sein de la fonction). Pour recoder cette fonction, on a besoin d’ajouter useCallback à l’import en haut du fichier avant d’encapsuler chacune de ces fonctions.

import React, { useState, useCallback } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = useCallback(event => {
    setUsername(event.target.value);
  }, []);

  const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  return <div></div> // we'll build up our response later
};

Lorsque l’utilisateur soumet le formulaire, on veut pouvoir envoyer le nom d’utilisateur et le nom du salon au serveur en échange d’un access token que l’on pourra utiliser pour entrer sur le salon. On va aussi créer cette fonction dans le composant.

On utilisera l’API fetch pour envoyer les données au format JSON au endpoint, recevoir et parser la réponse, puis on utilisera setToken afin de stocker le token dans notre état. On encapsulera aussi cette fonction avec useCallback, mais dans ce cas, la fonction dépendra du username et du roomName, donc ajoutons-les comme dépendances de useCallback.

  const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  const handleSubmit = useCallback(async event => {
    event.preventDefault();
    const data = await fetch('/video/token', {
      method: 'POST',
      body: JSON.stringify({
        identity: username,
        room: roomName
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    }).then(res => res.json());
    setToken(data.token);
  }, [username, roomName]);

  return <div></div> // we'll build up our response later
};

Pour la dernière fonction de ce composant, nous allons ajouter une fonctionnalité logout qui éjectera l’utilisateur d’un salon et les redigera sur le lobby. Pour ce faire, nous allons configurer le token sur null. et on l’encapsulera dans useCallback sans dépendances.

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);

  return <div></div> // we'll build up our response later
};

Ce composant dirige majoritairement les composants sous lui, donc il n’y a pas grand chose à afficher avant de les avoir créer. Créons d’abord le composant Lobby qui affiche le formulaire demandant un nom d'utilisateur et le nom du salon.

Le composant Lobby

Créons un nouveau fichier dans src/Lobby.js. Ce composant n’a besoin de stocker aucune donnée comme il passera tous les éléments à son confrère, le composant VideoChat. Quand le composant est rendu, il recevra le username et le roomName ainsi que les fonctions pour s’occuper de changer chacune d’entre elles et de soumettre le formulaire. On peut déstructurer ces props pour faciliter leur utilisation plus tard.

La tâche principale du composant Lobby est d’afficher le formulaire en utilisant les props comme suit :

import React from 'react';

const Lobby = ({
  username,
  handleUsernameChange,
  roomName,
  handleRoomNameChange,
  handleSubmit
}) => {
  return (
    <form onSubmit={handleSubmit}>
      <h2>Enter a room</h2>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="field"
          value={username}
          onChange={handleUsernameChange}
          required
        />
      </div>

      <div>
        <label htmlFor="room">Room name:</label>
        <input
          type="text"
          id="room"
          value={roomName}
          onChange={handleRoomNameChange}
          required
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

export default Lobby;

Mettons à jour le composant VideoChat pour afficher le Lobby - à moins que nous ayons un token, auquel cas on affichera le username, roomName et token. Nous avons besoin d’importer le composant Lobby en haut du fichier et de rendre un peu de JSX au bas de la fonction composant :

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <div>
        <p>Username: {username}</p>
        <p>Room name: {roomName}</p>
        <p>Token: {token}</p>
      </div>
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};

Pour avoir ce rendu sur la page, nous avons aussi besoin d’importer le composant VideoChat dans le composant App et l’afficher. Ouvrons src/App.js à nouveau et effectuons les changements suivants :

import React from 'react';
import './App.css';
import VideoChat from './VideoChat';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <VideoChat />
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛️
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

Assurons-nous que l’application s’exécute toujours (ou relançons-la avec npm run dev). Ouvrons la dans le navigateur et on verra un formulaire. Il faut alors remplir les infos de nom d’utilisateur et de salon, cliquer sur submit puis la vue devrait changer pour nous afficher les noms choisis en plus du token récupéré par le serveur.

Remplissez le formulaire et soumettez-le et vous verrez le nom d&#x27;utilisateur, le nom de la salle et le jeton sur la page.

Le composant Room

Maintenant que nous avons ajouté un nom d’utilisateur et de salon à l’application, on peut s’en servir pour rejoindre un salon de discussion Vidéo Twilio. Pour travailler avec le service Twilio Video, on aura besoin de JS SDK. Pour cet exemple, nous allons travailler avec la version Twilio Video 2.2.0. Installons-la avec :

npm install twilio-video@^2.2.0 --save

Créons un nouveau fichier appelé Room.js dans le répertoire src. Lancez-le avec le boilerplate suivant.

On aura besoin d’utiliser le SDK Twilio Video dans ce composant ainsi que les hooks useState  et useEffect. Nous allons aussi prendre comme props les roomName, token, et handleLogout du composant parent VideoChat.

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';

const Room = ({ roomName, token, handleLogout }) => {

};

export default Room;

La première chose que le composant va faire est de se connecter au service Twilio Video en utilisant le token et le nom du salon. Une fois que nous sommes connectés, nous obtenons un objet room que nous voulons stocker. Le salon comporte aussi une liste de participants qui changera au fur et à mesure, donc nous les stockerons également. Pour ce faire, on utilisera useState, les valeurs initiales seront null pour le salon, et un tableau vide pour les participants:

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);
});

Avant de rejoindre le salon, rendons quelque chose pour ce composant. On pourra alors parcourir le tableau pour rendre visible l’identité de chaque participants et aussi montrer l’identité du local des participants dans le salon:

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);

  const remoteParticipants = participants.map(participant => (
    <p key={participant.sid}>{participant.identity}</p>
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <p key={room.localParticipant.sid}>{room.localParticipant.identity}</p>
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});

Mettons à jour le composant VideoChat pour afficher ce composant Room à la place des informations placeholder que l’on avait tout à l’heure.

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';
import Room from './Room';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <Room roomName={roomName} token={token} handleLogout={handleLogout} />
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};

En exécutant ce code dans le browser, le nom du salon et le bouton de déconnexion s’afficheront mais nous n’aurons pas l’identité des participants car nous n’avons pas encore connecté et rejoint le salon.

Lorsque vous soumettez le formulaire maintenant, vous verrez le nom de la salle, "Super salle" dans ce cas, et un espace pour les participants à distance.

Nous avons toutes les informations nécessaires pour rejoindre le salon, donc nous devrions déclencher l’action de connexion dès le premier render du composant. On veut aussi pouvoir quitter le salon une fois que le composant est détruit (aucun intérêt à garder une connexion WebRTC en arrière-plan). Ce sont tous les deux des effets secondaires.

Avec des composants de classe, on utiliserait les méthodes de cycle de vie componentDidMount et componentWillUnmount. Avec React Hooks, on utilisera le hook useEffect.

useEffect

La fonction useEffect prend une méthode et l’exécute une fois que le composant s’est affiché. Lorsque notre composant charge, on veut pouvoir se connecter au service vidéo. On aura aussi besoin des fonctions que l’on peut exécuter dès qu’un participant rejoint ou quitte le salon, pour les ajouter et les enlever du state.

Commençons à coder notre hook en ajoutant ce code avant le JSX dans Room.js:

useEffect(() => {
    const participantConnected = participant => {
      setParticipants(prevParticipants => [...prevParticipants, participant]);
    };
    const participantDisconnected = participant => {
      setParticipants(prevParticipants =>
        prevParticipants.filter(p => p !== participant)
      );
    };
    Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.on('participantDisconnected', participantDisconnected);
      room.participants.forEach(participantConnected);
    });
  });

Le code utilise le token et le roomName pour se connecter au service Twilio Video. Une fois que la connexion est établie, on configure l’état du salon et on met en place un Listener pour les autres participants qui se connectent ou se déconnectent. Puis à l’aide de la fonction participantConnected que nous avons codé plus tôt, on passe dans une boucle sur la liste des participants pour les rajouter au tableau des participants

C’est un bon début mais si l’on enlève le composant, nous serons toujours connectés à la room. Donc nous avons besoin de nettoyer derrière nous aussi.

Si nous retournons une fonction depuis le callback que nous passons à useEffect, elle sera exécutée quand le composant sera démonté. Quand un composant qui utilise useEffect est rendu, cette fonction est également appelée pour nettoyer l’effet avoir qu’il ne soit exécuté à nouveau.

Retournons donc une fonction qui arrêtera le suivre de toutes les pistes des participants et puis les déconnectera du salon, si le participant est localement connecté:

    Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.participants.forEach(participantConnected);
    });

    return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.localParticipant.tracks.forEach(function(trackPublication) {
            trackPublication.track.stop();
          });
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  });

Notez que l’on utilise ici la version callback de la fonction setRoom que l’on a récupérée de useState tout à l’heure. Si vous passez une fonction sur setRoom, alors elle sera appelée avec les valeurs précédentes - dans notre cas, le salon existant que l’on appellera currentRoom - et cela configurera l’état à ce que nous rendrons.

Malgré tout ça, on n’a pas encore tout à fait fini. Dans son état actuel, le composant quittera et rejoindra le salon à chaque fois qu’il est rendu à nouveau. Ce n’est pas l’idéal, donc nous avons besoin de lui dire quand il devrait nettoyer et exécuter l’effet à nouveau. Un peu comme avec useCallback, on peut le faire en passant un tableau de variables dont l’effet dépend. Si les variables ont changé, on veut d’abord pouvoir nettoyer, puis exécuter l’effet à nouveau. Si elles n’ont pas changé, il n’y pas besoin d’encore exécuter l’effet.

En regardant la fonction, on peut voir que là où il y a le roomName ou token à changer, on s’attend à se connecter à un salon différent, ou comme un utilisateur différent. On va aussi passer ces variables sous forme de tableau à useEffect :

    return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.localParticipant.tracks.forEach(function(trackPublication) {
            trackPublication.track.stop();
          });
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  }, [roomName, token]);

Notez que nous avons deux fonctions de rappel définies au sein de cet effet. Vous pourriez penser qu’elles devraient être wrappées dans useCallback, comme nous l’avons fait plus tôt, mais ce n’est pas le cas. Comme elles font partie de l’effet, elles ne s’exécuteront que lorsque les dépendances se mettront à jour. Aussi, vous ne pouvez pas utiliser de hooks à l’intérieur des fonctions de rappel, elles doivent être utilisées directement au sein des composants ou d’un hook personnalisé.

On a plus ou moins fini avec ce composant, mais regardons d’abord si il fonctionne. Rechargeons/rafraichissons l’application et entrons un nom d’utilisateur et un nom de salon. On devrait voir notre identité apparaître alors qu’on rejoint le salon. Cliquer sur le bouton de déconnexion nous ramènera directement au lobby.

Désormais, lorsque vous soumettez le formulaire, vous verrez tout ce qui précède, ainsi que votre propre identité.

La pièce finale du puzzle est d’afficher les participants dans l’appel vidéo, ajouter leur vidéo et audio à la page.

Le composant Participant

Create a new component in src called Participant.js. We'll start with the usual boilerplate, although in this component we're going to use three hooks, useState and useEffect, which we've seen, and useRef. We'll also be passing a participant object in the props and keeping track of the participant's video and audio tracks with useState:

Créez un nouveau composant appelé Participant.js dans src. On commencera avec notre boilerplate habituelle bien que nous utiliserons trois hooks dans ce composant : useState et useEffect que l’on connaît déjà, et useRef. On va passer un objet participant dans les props et on gardera la trace des pistes vidéos et audios des participants avec useState:

import React, { useState, useEffect, useRef } from 'react';

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);
};

export default Participant;

Pour l’instant, affichons le jsx que nous voulons. Pour lier l’élément JSX a la ref, nous utilisons l’attribut ref.

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);

  const videoRef = useRef();
  const audioRef = useRef();

  return (
    <div className="participant">
      <h3>{participant.identity}</h3>
      <video ref={videoRef} autoPlay={true} />
      <audio ref={audioRef} autoPlay={true} muted={true} />
    </div>
  );
 });

J’ai aussi réglé les attributs des tags <video> et <audio> sur autoplay (afin qu’ils se lancent dès qu’ils ont un stream média) et sur muted (pour éviter de finir sourd avec les retours pendant la phase de test, vous me remercierez si jamais vous faites cette erreur).

Ce composant ne peut pas faire grand chose sans utiliser d’effets. On utilisera donc trois fois le hook useEffect dans ce composant, vous comprendrez vite pourquoi.

Le premier hook useEffect va configurer les pistes vidéo et audio dans le state et paramétrer les Listeners dans l’objet participant, pour lorsque les pistes sont ajoutées ou retirées. useEffect aura également besoin de nettoyer et retirer ces Listeners et vider l’état lorsque que le composant est démonté.

Nous ajouterons deux fonctions dans notre premier hook useEffect. Elles s’exécuteront soit quand une piste de participant est ajoutée, soit quand elle est retirée. Ces deux fonctions vérifient si la piste est un audio ou une vidéo, puis l’ajoutent ou la retirent de l’état en utilisant la fonction de state pertinente.

const videoRef = useRef();
  const audioRef = useRef();

  useEffect(() => {
    const trackSubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => [...videoTracks, track]);
      } else {
        setAudioTracks(audioTracks => [...audioTracks, track]);
      }
    };

    const trackUnsubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => videoTracks.filter(v => v !== track));
      } else {
        setAudioTracks(audioTracks => audioTracks.filter(a => a !== track));
      }
    };

    // more to come

Ensuite, nous utilisons l’objet participant pour configurer les valeurs initiales des pistes audio et vidéo. L’objet a des propriété videoTracks et audioTracks qui renvoient un Map des objets TrackPublication. Tant qu’il n’est pas inscrit, un TrackPublication n’a pas accès à son objet track . Nous avons donc besoin de filtrer toutes les pistes qui n’existent pas. Pour ce faire, on utilisera la fonction qui map depuis TrackPublication vers Track et on filtrera les pistes qui sont null.

Ensuite, on installe les Listeners aux événements trackSubscribed et trackUnsubscribed, en utilisant les fonctions que l’on vient d’écrire, avant de faire le nettoyage dans la fonction renvoyée :

  const trackpubsToTracks = trackMap => Array.from(trackMap.values())
    .map(publication => publication.track)
    .filter(track => track !== null);

  useEffect(() => {
    const trackSubscribed = track => {
      // implementation
    };

    const trackUnsubscribed = track => {
      // implementation
    };

    setVideoTracks(trackpubsToTracks(participant.videoTracks));
    setAudioTracks(trackpubsToTracks(participant.audioTracks));

    participant.on('trackSubscribed', trackSubscribed);
    participant.on('trackUnsubscribed', trackUnsubscribed);

    return () => {
      setVideoTracks([]);
      setAudioTracks([]);
      participant.removeAllListeners();
    };
  }, [participant]);

  return (
    <div className="participant">

Notez que le hook dépend uniquement de l’objet participant et ne sera pas nettoyé et ré-exécuté à moins que le participant ne change.

Nous avons aussi besoin d’un hook useEffect pour attacher les pistes audio et vidéo au DOM. Je ne vous  en montre qu’un ici, la version vidéo, mais c’est exactement le même processus pour l’audio, il faut juste entrer “audio” à la place de “video”.

Le hook va d’abord obtenir la première piste vidéo à partir de l’état et, si il existe, l’attacher au node DOM courant que nous avons pris avec un ref tout à l’heure. Vous pouvez vous référer au node DOM actuel dans le ref en utilisant videoRef.current. Si l’on attache la piste vidéo, on aura aussi besoin de rendre une fonction pour le détacher lors du nettoyage.

  }, [participant]);

  useEffect(() => {
    const videoTrack = videoTracks[0];
    if (videoTrack) {
      videoTrack.attach(videoRef.current);
      return () => {
        videoTrack.detach();
      };
    }
  }, [videoTracks]);

  return (
    <div className="participant">

Utilisez ce hook à nouveau pour audioTracks et nous voilà prêts à rendre notre composant Participant à partir du composant Room. Importons le composant Participant en haut du fichier, puis remplaçons les paragraphes qui affichaient l’identité avec le composant en lui-même.

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';
import Participant from './Participant';

// hooks here

  const remoteParticipants = participants.map(participant => (
    <Participant key={participant.sid} participant={participant} />
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <Participant
            key={room.localParticipant.sid}
            participant={room.localParticipant}
          />
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});

Maintenant, nous pouvons recharger l’application, joindre un salon, et l’on se verra sur l’écran ! Si on ouvre un autre browser et que l’on rejoint le même salon, on se verra à nouveau sur l’écran. Si on clique sur le bouton de déconnexion, on sera renvoyés dans le lobby.

Succès! Vous devriez maintenant vous voir dans un chat vidéo avec vous-même.

Conclusion

Construire avec Twilio Video dans React demande un peu plus de travail parce qu’il y a plein de sortes d’effets secondaires à gérer. Faire une requête pour avoir le token, se connecter au service Vidéo et manipuler le DOM pour connecter les éléments <video> et <audio>, il y a de quoi s’occuper. Dans cet article, nous avons vu comment utiliser useState, useCallback, useEffect et useRef pour contrôler ces effets secondaires et développer notre application en se servant uniquement de composants fonctionnels.

Avec un peu de chance, ce tutoriel vous aidera à comprendre le fonctionnement de Twilio Video et React Hooks. Tout le code source de l’appli est disponible sur GitHub pour que vous vous amusiez à démonter et reconstruire cette application.

Pour plus de lecture sur les ReactHooks, vous pouvez lire la documentation officielle, cette visualisation sur comment penser en ReactHooks et consulter l’article détaillé de Dan Abramov sur useEffect, c’est un post long mais qui vaut le coup, promis.

Si vous voulez en savoir plus sur ce que l’on peut construire avec Twilio Vidéo, lisez ces posts sur comment changer de caméra lors d’un chat vidéo ou ajouter un partage d’écrans à votre vidéochat.

Si vous construisez ces projets ou autre chose de cool qui embarque de la vidéo avec React, faites le moi savoir dans les commentaires, sur Twitter ou par email à  philnash@twilio.com.