React Hooks #2 : Les cas particuliers useContext, useReducer et useImperativeHandle

Vous avez bien assimilé les Hooks de base ? Et vous en voulez toujours plus? Voici quelques Hooks à ajouter à votre liste d’outils qui vous permettront de vous en sortir dans des cas particuliers.

Cet article est la partie 2 de la série sur les Hooks. Si vous avez raté le premier, je vous conseille d’aller y faire un tour : React Hooks #1 : La base avec useState, useEffect et useRef.

useContext

Avant de rentrer dans les détails du Hook, faisons un rappel sur ce qu’est le contexte dans React.

React Context

Context va vous permettre de partager des données entre vos composants; mais pas seulement entre parents et enfants.

A partir d’un composant Provider, vous allez pouvoir créer un “contexte” de données que tous les sous-composants (pas uniquement les enfants directs) vont pouvoir utiliser.

Sans l’utilisation de l’API Context, les informations sont passées de parents à enfants :

function App() {
  // On définit la valeur user
  const user = { name: "Kiwi", image: "avatar.png" };
  // Pour la passer au composant enfant
  return <Header user={user} />;
}

function Header({ user }) {
  return (
    <div>
      title
      {/* Et ainsi de suite */}
      <AccountIcon user={user} />
    </div>
  );
}

function AccountIcon({ user }) {
  const userName = user.name ? user.name : "Anonymous";
  const userImage = user.image ? user.image : "default.png";

  // Pour enfin l'utilisé
  return (
    <div>
      {userName}
      <img src={userImage} />
    </div>
  );
}

Dans cet exemple (tout à fait imaginaire), nous devons passer la valeur de user à travers plusieurs composants. Et encore, cet exemple est trivial, imaginez devoir passer ces données dans tous les composants de votre application.

C’est à cette question que répond Context.

Utilisation

useContext va nous permettre de récupérer ces valeurs. La syntaxe est la suivante:

const value = useContext(MonContext);

On a donc :

  • value : qui est la valeur stockée dans le contexte
  • MonContext : l’objet Context créé au préalable qui contient la valeur actuelle.

Création du contexte

L’objet Context est créé avec la méthode createContext de React. C’est cet objet qui sera utilisé dans tous les composants.

const MyContext = React.createContext(initialValue);

Avec useContext :

import React, { useContext, createContext } from "react";

// Définition du contexte avec une valeur par défaut
const UserContext = createContext({ name: "kiwi" });

function App() {
  // Récupération de la valeur du contexte
  const user = useContext(UserContext);

  return <span>{user.name}</span>;
}

Bon, c’est pas mal, mais comment on modifie ce contexte ? On reste avec notre variable user comme ça ? Non! On va utiliser les Provider.

Les Provider : définir la valeur du contexte

C’est grâce à cette fonctionnalité qu’on va commencer à utiliser Context vraiment. Alors comment ça se présente ?

import React, { useContext, createContext } from "react";

// Définition du contexte avec en valeur par défaut ``
const UserContext = createContext('');

function App() {
  const username = "kiwi";

  return (
    {/* Assignation d'une valeur au contexte */}
    <UserContext.Provider value={username}>
      <UserName />
    </UserContext.Provider>
  );
}

function UserName() {
  // Récupération de la valeur du contexte ({ name: "kiwi"})
  const username = useContext(UserContext);
  return <span>{username}</span>;
}

À partir de là on va pouvoir consommer la valeur user dans le composant App et faire redescendre les informations dans les composants utilisant le contexte. On peut notamment fournir la valeur issue d’un état.

function App() {
  const [user, setUser] = useState('');

  return (
    {/* Assignation d'une valeur au contexte */}
    <UserContext.Provider value={user}>
      <UserName />
      <input value={user} onChange={setUser}>
    </UserContext.Provider>
  );
}

L’exo

Je sens que vous commencez à être expert dans le domaine.

Dans cet exercice, il va falloir faire en sorte de modifier le contexte depuis un sous composant.

Les changements du champ UserNameInput doivent impacter le composant UserName. Il faudra utiliser un useState.

L’exercice -> useContext - Exercice

À noter

Plusieurs contextes

Comme pour les autres Hooks, vous pouvez utiliser plusieurs useContext dans le même composant.

function UserName() {
  const username = useContext(UserContext);
  const theme = useContext(ThemeContext);

  return <span style={{ color: theme.colors.primary }}> {username} </span>;
}

Provider vs valeur par défaut

Le Provider n’est pas nécessaire pour utiliser useContext. Dans le cas où un composant est en dehors du Provider, il prendra la valeur par défaut du contexte.

const UserContext = createContext("");

function App() {
  return (
    <>
      <UserContext.Provider value={"user"}>
        {/* Retourne : <span>user</span> */}
        <UserName />
      </UserContext.Provider>
      {/* Retourne : <span></span> */}
      <UserName />
    </>
  );
}

function UserName() {
  const username = useContext(UserContext);
  return <span>{username}</span>;
}

Solution

Voilà une des solutions possibles :

useReducer

Le concept de “reducer” a été popularisé par la librairie redux, qui centralise les données de l’application dans un unique objet state.

Vous connaissez useState, mais vous pouvez vous retrouver à devoir gérer des états plus complexes que de simples valeurs. useReducer va vous permettre de prendre en compte ces cas-là.

Tout d’abord, si c’est la première fois que vous rencontrez le terme “reducer”, ne vous inquiétez pas. Une fonction “reducer” est une fonction qui prend 2 valeurs en entrée et qui en retourne 1 en sortie. C’est aussi simple que ça.

Voilà, maintenant on peut passer à la syntaxe.

const [state, dispatchFunction] = useReducer(
  reducerFunction,
  initialArg,
  initFunction
);
  • state : la valeur actuelle retournée par useReducer
  • dispatch : la fonction qui va appeler la fonction reducerFunction avec un argument (dispatch(arg))
  • reducerFunction : la fonction qui va mettre à jour la valeur state et qui prends en paramètre la valeur state et l’argument envoyé par dispatch
  • initialArg : optionnel, l’argument qui sera passé à la fonction iniFunction. Sa valeur par défaut est null.
  • initFunction : optionnel, cette fonction est exécutée uniquement au montage du composant et initialise la valeur de state. Si initFunction n’est pas déclarée, elle renvoi la valeur initArg

Quoi de mieux qu’une bonne vielle application de “ToDo”. pour illustrer ce fonctionnement :

// Notre fonction "reducer" prenant en argument
// `state` la valeur actuelle du `useReducer et
// action l'argument passé par la fonction `dispatchTodo`
function todoReducer(state, action) {
  // On se basera sur la propriété `type` de l'objet envoyé par `dispatchTodo`
  // pour déterminer l'action à effectuer sur le `state`
  switch (action.type) {
    // si action.type === 'add', on ajoute la propriété todo au `state`
    case "add":
      return [...state, action.todo];
    // si action.type === 'add', on retire l'équivalent de la propriété todo du `state`
    case "remove":
      return state.filter((todo) => todo !== action.todo);
    // Sinon retourner la valeur actuelle (cela aura pour conséquence de ne pas réafficher le composant)
    default:
      return state;
  }
}

// La valeurs par défaut de notre liste
const initialTodo = [];

function Todo() {
  // Initialisation du `useReducer`
  const [todoItems, dispatchTodo] = useReducer(todoReducer, initialTodo);
  // `useState` pour gérer l'input
  const [inputValue, setInputValue] = useState("");
  const handleInputValue = (event) => setInputValue(event.target.value);

  const addTodo = () => {
    // Exécution de la fonction reducer avec l'action `{type: 'add', todo: inputValue}`
    dispatchTodo({ type: "add", todo: inputValue });
    // Réinitialisation de l'input, ça c'est pour faire joli
    setInputValue("");
  };

  // Exécution de la fonction reducer avec l'action `{type: 'remove', todo: todo}`
  const removeTodo = (todo) => dispatchTodo({ type: "remove", todo: todo });

  return (
    <div>
      <input value={inputValue} onChange={handleInputValue} />
      <button onClick={addTodo}>Ajouter</button>
      <ul>
        {/* On affiche les éléments de la Todo grâce à la valeur du `useReducer` : `todoItems`*/}
        {todoItems.map((todo) => (
          <TodoItem name={todo} />
        ))}
      </ul>
    </div>
  );
}

Que se passe-t-il donc ici?

Aux cliques sur le bouton Ajouter :

  1. La fonction addTodo est exécutée et va donc appeler successivement dispatchTodo et setInputValue
  2. La fonction dispatchTodo est exécuté avec { type: "add", todo: inputValue } en argument
  3. useReducer va donc exécuter sa fonction “reducer” avec state (égale à [] la valeur par défaut) et action (égale à { type: "add", todo: inputValue })
  4. action.type étant égale à 'add', la fonction retournera un tableau avec notre nouvel élément.
  5. La nouvelle valeur de todoItems est donc rafraichi
  6. Le rendu du composant se fait et notre todo est ajouté dans la vue

Le fait d’utiliser un objet avec un propriété type vient de l’utilisation de Redux. Cependant, vous pouvez faire comme bon vous semble.

Et comme toujours, vous pouvez en utiliser plusieurs dans un même composant.

L’exo

Dans cet exercice, nous allons utiliser useReducer pour le “sport”. D’autres options plus simples pourraient être mises en place, mais c’est une manière de vous embêter entrainer.

Vous allez faire le compteur mais avec un useReducer.

L’exercice -> useReducer - Exercice

À noter

Abandon de rendu

Comme pour useState, si la fonction “reducer” retourne la même valeur que la valeur actuelle, le rendu est abandonné. Et comme pour useState, c’est la méthode Object.is.

Solution

Voilà une des solutions possibles :

useImperativeHandle

Ce Hook, useImperativeHandle, est utilisé avec la fonctionnalité forwardRef de React. L’idée est de personnaliser la référence renvoyée par le composant dans lequel il est appelé.

Mais tout d’abord, un petit rappel sur forwardRef.

forwardRef

Pour récupérer la référence d’un composant du DOM, la propriété ref est utilisée. On l’a déjà vu dans l’utilisation de useRef dans mon article précédent : useRef.

function CustomInput() {
  const inputRef = useRef();
  return <input ref={inputRef} />;
}

forwardRef va permettre de transférer cette référence à un composant plus haut. Par exemple, si un composant parent à CustomInput souhaite effectuer un focus sur l’input, on peut renvoyer la référence via la propriété ref de CustomInput.

const CustomInput = forwardRef(function (props, ref) {
  // On renvoi la référence de l'input via la ref
  return <input ref={ref} />;
});

function Form() {
  const customInputRef = useRef();
  const focus = () => {
    // La fonction focus est utilisé depuis la référence de notre CustomInput
    customInputRef.current.focus();
  };
  return (
    <>
      {/* On récupère la référence du composant comme pour l'input */}
      <CustomInput ref={customInputRef} />
      <button onClick={focus}> Focus </button>
    </>
  );
}

Référence personnalisée

Avec useImpertiveHandle, on va pouvoir renvoyer ce que l’on souhaite en référence.

useImperativeHandle(ref, createHandle, [deps]);
  • ref : Le deuxième argument envoyé par forwardRef (forwardRef(function(props, ref /*<- celui-là*/) {)
  • createHandle : fonction retournant la nouvelle référence
  • [deps] : optionnel, un tableau de dépendances qui ré-executera la fonction createHandle
const CustomInput = forwardRef(function (props, ref) {
  // Initialisation du useRef pour récupérer la référence de l'input
  const inputRef = useRef();

  // Gestion de l'input
  const [inputValue, setInputValue] = useState("");
  const handleChange = (event) => setInputValue(event.target.value);

  useImperativeHandle(ref, () => ({
    // La fonction `focus` de notre composant sera la même que l'input
    focus: inputRef.current.focus(),
    // La fonction `clear` de notre composant sera une mise à jour de la valeur de l'input
    clear: () => setInputValue(""),
  }));

  return <input ref={inputRef} value={inputValue} onChange={handleChange} />;
});

Le tableau de dépendances peut contenir des propriétés passées au composant ou de valeurs d’état. La nouvelle référence sera renvoyée via forwardRef.

L’exo

Pour cet exercice, on va personnaliser la référence d’un composant audio.

Il faudra modifier le composant CustomAudio pour qu’il renvoi les fonctions play et forward. play fait partie de la référence du composant audio, mais la fonction forward est implémentée à côté.

L’exercice -> useImperativeHandle - Exercice

À noter

Utilisation de code impératif

React recommande d’utiliser le moins possible la modification directe des références. Dans la plupart des cas, il est possible de trouver une autre solution.

Solution

Voilà une des solutions :

Conclusion

J’imagine que c’est beaucoup d’informations à digérer. Mais vous pouvez revenir ici autant de fois que vous le souhaitez ;).

Et n’oubliez pas, ces Hooks sont vraiment là pour répondre à des cas particuliers. Maintenant vous avez avec vous ces nouveaux outils. Ils vous serviront justement si les autres ne répondent pas à vos attentes.

Et comme toujours, exercez-vous. C’est toujours simple au premier abord, mais souvent difficile à remettre en place. Les exercices sont là pour vous mettre en situation. N’hésitez à trouver d’autres solutions, utiliser plusieurs Hooks ou même à trouver vos propres exercices.

Bonne chance à vous pour la suite !

Références