6 Nov 2019 | Développement

Créer un hook custom pour interroger une API

Depuis l’ajout des Hooks dans la version 16.8 de React il est devenu aisé d’interroger une API depuis des composants fonctionnels. Plus besoin de sortir l’artillerie lourde avec des classes pour bénéficier d’un state et des cycles de vie des composants. Il est aussi beaucoup plus simple d’isoler notre code pour créer un hook custom que nous pourrons réutiliser dans plusieurs composants.

 

Nous n’aborderons ici que partiellement le détail de l’utilisation des hooks standards. La documentation officielle de React est un très bon point de départ si la notion de hook vous est inconnue.

L’objectif est de mettre en place une petite app de messagerie. Elle se limitera à afficher une liste des messages enregistrés ainsi qu’un petit formulaire pour en saisir de nouveaux. L’occasion de mettre en place un petit hook custom qui aura pour mission de gérer tout le code dédié à la manipulation des données externes via une API.

jsonbox.io

Pour notre exemple nous allons utiliser jsonbox.io. Ce service très pratique nous fournit un espace de stockage JSON ainsi qu’une API pour interroger nos données. Aucune inscription n’est requise !

Il suffit de vous rendre sur jsonbox.io pour récupérer l’URL de votre box personnelle. Ceci nous permettra d’avoir un backend clé en main et de nous focaliser sur notre hook. Mettez de côté votre URL, elle nous sera utile par la suite.

Home de jsonbox.io


codesandbox.io

L’application finale est disponible sur codesandbox.io, vous avez donc tout le loisir de jouer avec le code: collez l’url de votre jsonbox à l’endroit indiqué dans le fichier index.js et c’est parti !

Ouvrez l’app dans CodeSandbox


Commençons à créer un hook custom

Pour plus de clarté nous allons isoler le code de notre hook dans son propre fichier. Dans notre exemple, nous rangerons ce fichier dans un dossier spécifique, ce qui nous donnera : hooks/useJsonbox.js

Pour créer un hook custom l’idée est de regrouper toute la logique de notre code dans une seule et même fonction. Par convention, le nom d’un hook commence par use. Nous nommerons donc notre fonction useJsonbox.


const useJsonbox = jsonboxUrl => {
  // ...
  return { messages, isLoading };
};

export default useJsonbox;

Notre fonction prend en paramètre l’url de notre box et nous retourne 2 variables.

Dans un premier temps nous allons définir les variables dont nous aurons besoin dans le state local de notre fonction grâce au hook useState :


import { useState } from "react";

const useJsonbox = jsonboxUrl => {
  const [messages, setMessages] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  // ...
  return { messages, isLoading };
};

export default useJsonbox;

Nous commençons par déclarer la variable d’état messages qui correspond à la liste de nos futurs messages. La fonction setMessages quand à elle nous permettra de modifier messages. Nous initialisons notre variable avec un array vide que nous alimenterons avec nos messages.

Ensuite, nous déclarons isLoading et sa fonction de modification setIsLoading. Comme son nom l’indique, cette variable booléenne stocke l’état du chargement de nos messages. Elle nous permettra aussi bien d’afficher un preloader que de déclencher le chargement initial des données. C’est pour cette raison que nous la déclarons à true.


useEffect et chargement initial

Pour charger nos messages sans avoir à utiliser de librairie externe nous utiliserons l’API Fetch. Dans notre cas, nous désirons faire un GET sur notre url jsonboxUrl, voici à quoi ressemble notre appel :


fetch(jsonboxUrl, {
  method: "GET"
})
  .then(res => res.json())
  .then(data => {
    // ... traitement des données réceptionnées
  })
  .catch(error => console.log(error));
};

La réception des résultats de notre requête s’effectue de manière asynchrone. Il s’agit d’un effet de bord que nous devons gérer avec un autre hook standard: useEffect. Nous ajoutons donc useEffect à nos imports et ajoutons notre fetch sous cette forme :


import { useState, useEffect } from "react";

const useJsonbox = jsonboxUrl => {
  const [messages, setMessages] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  // ...
  // GET
  useEffect(() => {
    if (isLoading && jsonboxUrl) {
      fetch(jsonboxUrl, {
        method: "GET"
      })
        .then(res => res.json())
        .then(data => {
          setMessages(data);
          setIsLoading(false);
        })
        .catch(error => console.log(error));
    }
  }, [isLoading, jsonboxUrl]);
  // ...
  return { messages, isLoading };
};

export default useJsonbox;

Vous noterez au passage que nous avons emballé notre fetch dans un test : isLoading doit être à true et jsonboxUrl renseigné pour que notre fetch puisse être exécuté.

Nous ajoutons aussi un array en deuxième paramètre de notre useEffect avec ces 2 variables. Ainsi, notre effet n’est exécuté que lorsqu’au moins une des 2 variables changent.

Notre isLoading étant initialisé à true par défaut, notre code sera donc exécuté lors du premier chargement de notre hook, dès que le DOM sera prêt.

Examinons maintenant les 2 lignes de code exécutées en réponse à notre requête :


          setMessages(data);
          setIsLoading(false);

data contient la réponse retournée par l’API. Il s’agit d’un array contenant les messages existants. En passant cet array en argument de setMessages nous modifions notre state messages : Nous avons nos messages dans le state !

Une fois ceci fait, il faut signaler à notre application que le chargement est terminé. C’est la raison d’être de notre setIsLoading(false); qui bascule notre isLoading à false.


Utilisation de base

A ce stade, notre hook custom, bien que limité, est fonctionnel. A titre d’exemple voici comment nous pourrions l’implémenter dans une application basique :


import React from "react";
import ReactDOM from "react-dom";
// import de notre hook
import useJsonbox from "./hooks/useJsonbox";

import "./styles.css";

function App() {
  // Utilisation de notre hook avec l'url de votre jsonbox (https://jsonbox.io)
  // Vous devez renseigner la variable ci-dessous avec votre propre url
  const jsonboxUrl = "";

  const { messages, isLoading } = useJsonbox(jsonboxUrl);

  return (
    <div className="App">
      {jsonboxUrl && isLoading && <p>Chargement</p>}
      {jsonboxUrl && !isLoading && !messages.length && <p>Pas de message</p>}
      <ul>
        {jsonboxUrl &&
          !isLoading &&
          messages &&
          messages.map(message => (
            <li key={message._id}>
              {message.name} : {message.desc}
            </li>
          ))}
      </ul>
      // [...]
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(, rootElement);

Le code est disponible dans cette sandbox intermédiaire.

Pensez à renseigner l’URL de votre box à cette ligne : const jsonboxUrl = "";

Dans un premier temps nous importons notre hook : import useJsonbox from "./hooks/useJsonbox";

Nous pouvons ensuite utiliser useJsonbox dans le corps de l’app sous cette forme: const { messages, isLoading } = useJsonbox("https://...");

A noter que nous passons l’url de notre jsonbox en paramètre de notre hook.
Ceci fait, messages et isLoading sont disponible et peuvent être utilisés dans le reste de l’app.


Schéma d’un message

L’affichage de nos messages est donc mis en place seulement nous n’avons pas encore de message à afficher… et nous ne savons même pas trop à quoi ils ressembleront !

Dans l’exemple ci-dessus nous affichons le détail de nos messages sous cette forme : {message.name} : {message.desc}

Ceci est arbitraire et correspond au schéma de l’objet par défaut que propose jsonbox.io à l’initialisation d’une nouvelle box. Nous allons conserver ce schéma, nos messages une fois retournés par l’API ressembleront donc à ceci :


  {
    "_id": "5db9df70734c080017c25b1b",
    "name": "jsonbox.io",
    "desc": "Lorem ipsum dolor sit amet.",
    "_createdOn": "2019-10-30T19:07:28.497Z"
  }

Nos messages ont donc 2 champs name et desc qui correspondent respectivement au pseudo de l’auteur et au corps de son message. Les deux autres champs sont des champs internes générés par l’API.


Ajout d’un message depuis le dashboard jsonbox.io

Pour tester, nous pouvons ajouter un message depuis le dashboard de jsonbox.io. Si ce n’est pas déjà fait, vous pouvez cliquer sur le bouton « Go to dashboard » depuis la home de votre box.

Depuis le dashboard, sélectionner le verbe « POST » dans le menu déroulant à gauche de l’url de votre box puis adaptez l’objet json se trouvant dans la zone d’édition de gauche, sous l’intitulé « Your POST/PUT request body: ». Pensez à respecter le nom des champs ! Nous aurons besoin de notre name et de notre desc par la suite.

Vous pouvez désormais poster votre message en cliquant sur le bouton bleu « GO » figurant à droite de l’URL de votre box. Si tout se passe bien vous recevrez un message provenant de l’API représentant votre message nouvellement ajouté.

A ce stade, notre application ressemble à cette sandbox intermédiaire. Le code est similaire à peu de chose près au code de notre exemple précédent.

Une fois l’URL de votre jsonbox renseignée, vous devriez voir votre message apparaître dans la liste !


Implémentation de l’ajout de message dans notre hook

Dans un premier temps, nous allons implémenter l’ajout de message dans notre hook. Ensuite nous créerons un petit formulaire qui permettra de saisir de nouveaux messages directement depuis notre petite application.

Pour y voir plus clair, vous pouvez consulter cette sandbox intermédiaire contenant les modifications et ajouts qui vont suivre à cette étape.

Commençons donc par modifier notre hook.
Pour ajouter un nouveau message nous allons mettre en place une requête POST vers l’API ayant pour body le contenu du message. Comme pour notre GET, il s’agit d’un effet de bord qu’il va falloir prendre en charge par un hook useEffect et que l’on rendra réactif grâce à une nouvelle variable dans notre state.

Voyons ce que ça donne :


const useJsonbox = jsonboxUrl => {
  // ...
  const [newMessage, setNewMessage] = useState(false);

  // GET
  // ...

  // POST
  useEffect(() => {
    if (newMessage && newMessage.name && newMessage.desc && jsonboxUrl) {
      fetch(jsonboxUrl, {
        method: "POST",
        headers: new Headers({
          Accept: "application/json",
          "Content-Type": "application/json"
        }),
        body: JSON.stringify(newMessage)
      })
        .then(res => res.json())
        .then(data => {
          setNewMessage(false);
          setMessages([data].concat(messages));
        })
        .catch(error => console.log(error));
    }
  }, [messages, newMessage, jsonboxUrl]);

  return { messages, isLoading, setNewMessage };
};

Nous avons donc ajouté une nouvelle variable newMessage à notre state qui prendra pour valeur le contenu de notre message. Cette variable sera modifiable grâce à la fonction associée setNewMessage

Ensuite, attaquons-nous à notre nouveau bloc useEffect.
A la différence de notre GET, nous devons renseigner le header de notre fetch et attribuer le contenu du message au body de la requête.

Enfin, nous pouvons traiter la réponse retournée par l’API pour gérer notre nouveau message: Il nous suffit d’utiliser setMessage pour concaténer notre nouveau message (data) avec notre liste actuelle de message : [data].concat(messages)

Nous utilisons aussi notre fonction setNewMessage nouvellement créée pour remettre à zéro notre state newMessage.

A noter aussi que nous avons adapté les variables qui déclencheront notre hook : dans l’array en deuxième paramètre de notre useEffect nous avons ajouté notre newMessage. Ce sont les changements de ce state qui déclencheront l’ajout de notre message !

Dernier point, n’oublions pas de retourner notre setNewMessage pour qu’il soit disponible dans le reste de l’app. Nous pourrons ainsi l’utiliser dans notre formulaire.

C’est bien joli mais nous n’avons pas moyen pour l’instant de tester ! Nous avons posé le traitement de l’arrivée d’un nouveau message mais il nous manque le moyen de poster ce message. Vous l’aurez deviné, c’est l’objet du formulaire que nous allons créer !


Formulaire de soumission d’un message

Histoire de ne pas trop polluer notre index nous allons créer un composant dédié au formulaire qui aura pour chemin : components/form.js

Nous allons aussi préparer le terrain en intégrant à l’avance notre formulaire dans notre index.js, au dessus de la liste des messages, sous cette forme :


{jsonboxUrl && <Form setNewMessage={setNewMessage} />}

Vous noterez que l’on passe notre setNewMessage dans les props du formulaire.

Voici le code du formulaire correspondant :


import React, { useState } from "react";

const Form = ({ setNewMessage }) => {
  // les différentes vars dont nous aurons besoin dans notre state local
  const [name, setName] = useState("");
  const [desc, setDesc] = useState("");
  const [error, setError] = useState("");

  // Soumission du formulaire, si name et desc sont définis
  const submitItem = (name, desc) => {
    if (!name || !desc) {
      setError("Pseudo and Message are required");
    } else {
      setNewMessage({ name, desc });
      setError("");
      setDesc("");
    }
  };

  return (
    <div style={{ marginBottom: "1rem" }}>
      <input
        type="text"
        id="name"
        value={name}
        onChange={e => setName(e.target.value)}
        autoFocus
        maxLength={25}
        placeholder="Votre pseudo"
        style={{ marginBottom: "1rem" }}
      />

      <input
        type="text"
        id="desc"
        value={desc}
        onChange={e => setDesc(e.target.value)}
        maxLength={250}
        placeholder="Votre message"
      />
      {error && <p style={{ color: "#f00" }}>{error}</p>}

      <button
        onClick={() => submitItem(name, desc)}
        className="submit"
        disabled={!name || !desc}
      />
        Envoyer
      </button>
    </div>
  );
};

export default Form;

Pas de grosse surprise ici…
Nous définissons les variables dont nous aurons besoin localement dans le composant, à savoir name, desc et error ainsi que les fonctions qui permettront de les modifier.

error nous servira à afficher un message d’erreur si le formulaire est soumis et que l’un de nos deux champs est vide.

Le formulaire à proprement parler se compose simplement de 2 champs de saisie et d’un bouton de soumission.
Nous attribuons le contenu de name à la propriété value de notre premier champ de saisie. Mais cela ne suffit pas, nous voulons qu’à chaque modification de ce champ par l’utilisateur la valeur de notre state soit mise à jour. Nous ajoutons donc un event onChange qui déclenchera la mise à jour du state grâce à notre fonction de modification setName

Nous retrouvons un traitement similaire pour notre deuxième champ avec desc et setDesc.

Enfin, notre bouton de soumission déclenche l’exécution d’une fonction sumbitItem qui se chargera dans un premier temps d’évaluer si name et desc ont bien été renseigné. Si ce n’est pas le cas, il chargera un message d’erreur dans notre state error qui déclenchera l’affichage d’un message adapté.

Si name et desc sont correctement renseignés, il utilisera notre fonction setNewMessage pour déclencher notre hook ! Vous vous souvenez ? Si newMessage existe, le useEffect de notre hook custom déclenche le POST du message.


Créer un hook custom : Faisons le point

Notre hook custom est désormais fonctionnel. Toute la logique relative à l’API externe de jsonbox.io est isolée et notre hook est facilement réutilisable.

Pour créer un hook custom nous avons commencé par gérer la récupération et l’affichage de la liste des messages : Sandbox – étape 1

Nous avons ensuite mis en place l’ajout de message dans notre hook ainsi qu’un petit formulaire permettant de saisir un nouveau message : Sandbox – étape 2

Petit bonus

La sandbox finale dispose de quelques fonctionnalités supplémentaires :

  • La suppression de message a été implémentée dans le hook
  • Un petit générateur d’URL jsonbox.io est intégré

Ouvrez l’app finale dans CodeSandbox


Pour aller plus loin

Cet exemple n’est pas particulièrement optimisé … Pour aller plus loin, nous pourrions par exemple factoriser nos différents fetchs pour en faire une fonction unique qui serait à même de gérer tous nos cas de figures. Tous nos effets utiliseraient alors cette fonction que nous pourrions mémoizer en utilisant le hook standard useCallback.

N’hésitez pas à me faire part de vos remarques, suggestions et autres questions via twitter !

Amusez-vous bien 🙂

Vous cherchez un développeur JS ?

N’hésitez pas à me contacter pour mettre en place votre future application React