Lettres de scrabble formant le mot « apprendre » en anglais

Comment écrire un composable avec Vue.js

Les Composables dans Vue.js sont des fonctions réutilisables qui encapsulent une logique d'état et son comportement, permettant de partager du code entre composants de manière modulaire.

Vous avez entendu parler des composables Vue.js et vous voulez écrire les vôtres ? Peut-être avez-vous déjà utilisé des composables écrits par d’autres, mais vous doutez de la démarche à suivre commencer à en créer un pour vous-même. C’est l’objet de cet article !

Qu’est-ce qu’un composable dans Vue.js

Un composable Vue s’apparente à un utilitaire, avec une différence importante : il contient un état, c’est-à-dire des données définies avec la fonction reactive ou ref de Vue.

L’exemple utilisé dans la documentation de Vue.js correspond à une fonction composable useMouse. On expose les données réactives x et y par la fonction composable. Cela signifie qu’à chaque fois que les données sont mises à jour, cela déclenche la mise à jour de l’interface utilisateur (UI) des éléments DOM correspondants. Ici, vous pouvez voir l’effet du composable useMouse

Le point vraiment intéressant des composables réside dans le fait que nous pouvons bénéficier de la définition de données réactives (tout comme dans un composant), mais sans avoir à présenter une UI ! Cela signifie que nous pouvons abstraire la logique en dehors d’un composant et la réutiliser dans le contexte d’une variété de composants différents (ou même sans aucun composant).

Vous pouvez avoir une bonne idée de ce qui est possible en consultant la documentation de VueUse. Il s’agit d’une collection de plus de 200 composables immédiatement utilisables qui résolvent des problématiques courantes telles que le travail avec des données dans localStorage, le basculement du mode dark, et bien plus encore.

Comment définir un composable avec Vue

Maintenant que vous avez une bonne idée à quoi correspond un composable, voyons étape par étape comment en créer un.

La première étape est de créer un fichier pour votre composable. La convention nous indique qu’il faut le stocker dans un répertoire appelé *composables*.

Dans ce fichier, vous allez exporter une fonction avec un nom qui décrit ce que fait votre composable. Pour cet article, nous allons créer un composable useMenu comme exemple.

1
2
// @/src/*composables*/useCycleElements.ts
export const useCycleElements = () => {};

Le préfixe use pour les composables

Notez que le nom du composable commence par use. C’est aussi une autre convention commune pour aider les utilisateurs de votre composable à le distinguer d’une fonction utilitaire normale et non étatique.

Écrire ses composables en TypeScript

C’est aussi une très bonne idée d’écrire vos composables en utilisant TypeScript. Cela les rend plus intuitifs à utiliser (autocomplétion, détection d’erreurs, etc.). Nous utiliserons TypeScript dans cet article, mais si vous n’êtes pas à l’aise avec TypeScript, vous pouvez toujours écrire vos composables en JS.

Accepter les arguments composables

Tous les composables ne nécessitent pas d’argument, mais la plupart en ont besoin. Pour notre exemple, prenons une liste de mots :

1
export const useCycleElements = (elements: any[]) => {};

Les arguments constituent l’interface (ou API) pour l’INPUT ou entrée du composable.

Retourner des données et des fonctions à partir d’un composable

Ensuite, définissons l’API de SORTIE de notre composable, c’est-à-dire ce qui est renvoyé par la fonction.

1
2
3
4
5
6
7
export const useCycleElements = (elements: any[]) => {
  return {
    prev,
    next,
    current,
  };
};

Ici, nous exposons 2 choses. Décomposons chacune d’entre elles.

current ou la donnée d’état

Nous avons ici une donnée réactive.

Par exemple, dans l’utilisation suivante, current pourrait valoir le premier mot dans la liste :

1
2
const { current } = useCycleElements("Bonjour", "cher", "lecteur !");
console.log(current); // logs "Bonjour"

Les fonctions next et prev

La fonction exposée next permettrait au consommateur du composable de passer à la valeur suivante dans la liste. Ainsi, avec le code suivant, current devient cher puis lecteur !.

1
2
3
4
5
6
const { current, next } = useCycleElements("Bonjour", "cher", "lecteur !");
console.log(current); // logs "Bonjour"
next();
console.log(current); // logs "cher"
next();
console.log(current); // logs "lecteur !"

La fonction prev réalise l’inverse de next.

Flux de conception des composables

Définir l’interface de notre composable (son API). Rien de tout cela ne fonctionne encore parce que nous devons encore implément la logique métier du composable. Mais ce n’est pas grave. Il s’agit en fait d’une très bonne méthode pour écrire un composable.

On définit comment l’on souhaite utiliser le composable (faites-en une DX, ou expérience de développement, agréable) AVANT d’implémenter les détails.

Cette approche de conception s’applique à tous les types de fonctionnalités (composants, magasins, etc.) et nous pouvons certainement l’appliquer aux composables également.

Définir un état réactif pour le composable

Maintenant, créons un état réactif pour savoir quel élément de la liste est actif. C’est ce qui en fait un composable et qui le rend utile dans le contexte d’une application Vue. Ce que nous voulons vraiment savoir, c’est la position de l’élément actif. Créons donc un ref réactif activeIndex.

1
2
3
4
5
import { ref } from "vue";
export const useCycleElements = (elements: any[]) => {
  const activeIndex = ref(0);
  // ...
};

Ensuite, nous pouvons créer des données dérivées réactives (c’est-à-dire une variable computed) pour déterminer ce que vaudra menuOpen en fonction de la valeur de isDesktop.

1
2
3
4
5
6
7
8
import { ref, computed } from "vue";
export const useCycleElements = (elements: any[]) => {
  const activeIndex = ref(0);
  // 👇 la variable `current` réactive est basé sur la valeur de `activeIndex`
  const current = computed(() => elements[activeIndex.value]);
  //...
  return { current /*...*/ };
};

Définir des fonctions exposées pour le composable

Avec cela, prev et next sont vraiment faciles à implémenter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export const useCycleElements = (elements: any[]) => {
  //...
  // 👇 la fonction next
  function next() {
    // si l'`état` est le dernier élément, commencer au début de la liste
    if (activeIndex.value === elements.length - 1) {
      activeIndex.value = 0;
    } else {
      // sinon, il suffit d'incrémenter l'indice actif de 1
      activeIndex.value += 1;
    }
  }
  // 👇 la fonction prev
  function prev() {
    // si `state` est le premier élément, il faut l'enrouler jusqu'à la fin
    if (activeIndex.value === 0) {
      activeIndex.value = elements.length - 1;
    } else {
      // sinon, il suffit de décrémenter l'indice actif de 1
      activeIndex.value -= 1;
    }
  }
  return { /*...*/ next, prev };
};

Fournir au composable des arguments réactifs

Une chose importante à prendre en compte lors de l’écriture d’un composable est que les gens travaillent souvent avec des données réactives dans leurs composants. Ils s’attendent ainsi à pouvoir passer intuitivement ces données réactives dans n’importe quel composable.

En d’autres termes, ils peuvent vouloir faire ceci :

1
2
const sentence = ref(["Bonjour", "cher", "lecteur !"]);
const { current, next } = useCycleElements(sentence);

Mettons donc à jour le composable pour qu’il accepte une liste réactive (une liste définie avec ref ou reactive).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { /* ... */, watch, type Ref } from "vue";
export const useCycleElements = (elements: Ref<string[]>) => {
  //...
  // Et ensuite, tout au long du *composable*, vous devrez remplacer toutes les utilisations de
  // `elements` par `elements.value`
  // par exemple 👇
  const current = computed(() => elements.value[activeIndex.value]);
  // faire de même pour list dans next, prev, etc...
  // 👇 enfin, puisque la liste peut changer,
  // exécutons un petit nettoyage sur activeIndex
  // si la liste est remplacée par quelque chose de plus court
  watch(elements, () => {
    if (activeIndex.value >= elements.value.length) {
      activeIndex.value = 0;
    }
  });
  // ...
};

Permettre au composable d’accepter des arguments non réactifs ET réactifs (plus des Getters !)

Ce qui précède est parfait pour accepter une liste réactive. Mais maintenant, nous avons OBLIGÉ le consommateur composable à passer quelque chose de réactif.

Certains peuvent préférer la facilité de passer un simple tableau quand cela s’avère nécessaire. Pas de soucis ! Nous pouvons supporter les deux avec une fonction d’aide de Vue appelée toRef.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { ref, computed, toRef, watch } from "vue";
import { type MaybeRefOrGetter } from "vue";
// 👇 nous utilisons le type `MaybeRefOrGetter`, natif à Vue
export const useCycleElements = (elements: MaybeRefOrGetter<any[]>) => {
  // l'appel à toRef normalise la liste en ref (quelle que soit la manière dont elle a été transmise)
  const reactiveElements = toRef(elements);
  // remplacer toutes les utilisations de elements.value
  // par reactiveElements.value
  // par exemple 👇
  const current = computed(() => reactiveElements.value[activeIndex.value]);
  // faire de même pour list dans next, prev, watch, etc...
  //...
};

Maintenant nous pouvons supporter les deux types de données pour cette liste, et nous obtenons le support d’un troisième type de données GRATUITEMENT ! Tout ce qui suit fonctionne maintenant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Comme des données simples
const { current, prev, next } = useCycleElements([
  "Bonjour",
  "cher",
  "lecteur !",
]);
// En tant que données réactives
const list = ref(["Bonne", "journée", " à vous !"]);
const { current, prev, next } = useCycleElements(list);
// En tant que getter
const list = ref(["A bientôt", "sur", "mon blog"]);
const { current, prev, next } = useCycleElements(() => list.value);

Vous pouvez en savoir plus sur toRef dans la documentation de Vue.js. De plus, d’autres fonctions similaires existent pour créer des composants flexibles et robustes : toValue et isRef, entre autres.

Améliorer l’API du composable avec une propriété computed modifiable

Actuellement, si nous essayons de définir l’état (current) à partir du composant, cela ne fonctionnera pas. Pourquoi ? Nous le définissons comme une propriété calculée ou computed.

1
const current = computed(() => reactiveElements.value[activeIndex.value]);

Pour rendre l’API un peu plus flexible, on pourrait modifier le code pour mettre à jour la valeur de l’index courant dans la liste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const current = computed({
  // la méthode `get` est appelée lorsque l'état est lu
  get() {
    // c'est la même chose que le retour de la fonction précédente
    return reactiveElements.value[activeIndex.value];
  },
  // la méthode set est appelée lorsque l'état est écrit
  set(value) {
    // prend la valeur donnée et l'applique à l'élément du tableau à l'index actif
    reactiveElements.value[activeIndex.value] = value;
  },
});

Rendre le composable TypeSafe

Jusqu’à présent, nous avons utilisé string[] pour définir l’argument composable word. C’est assez logique, car nous voulons que notre composable fonctionne pour un tableau de n’importe quoi (pas seulement des chaînes de caractères comme dans les exemples).

Cependant, la plupart des utilisateurs de TypScript savent que l’utilisation de any ne rend pas le linter TypeScript très content. Sa présence signifie qu’on peut encore améliorer le code.

Utilisons donc un générique pour dire que chaque élément du tableau est un type de variable.

1
2
// T est le générique ici
export const useCycleElements = <T>(list : MaybeRefOrGetter<T[]>) => {

En quoi cela est-il utile ?

Maintenant, TypeScript peut déduire que notre état est un WritableComputedRef du MÊME type que les éléments de la liste passée. Dans notre exemple, nous utilisons une chaîne de caractères.

Ce n’est pas une fonctionnalité 100% nécessaire, mais cela peut s’avérer utile en pensant à la façon dont tous les utilisateurs pourraient utiliser votre composable et de le rendre aussi intuitif que possible.

Conclusion

Les composables sont un outil puissant pour créer une logique réutilisable et avec un état dans vos applications Vue.js. Ils s’avèrent plus faciles à écrire que vous ne le pensez. Bien que VueUse fournisse une vaste gamme de composables pré-écrits pour vous, il y a certainement des moments où vous aurez besoin d’écrire les vôtres. Vous savez maintenant comment faire !

Suivez-moi !

Merci d’avoir lu cet article. Assurez-vous de me suivre sur X, de vous abonner à ma publication Substack et d’ajouter mon blog à vos favoris pour ne pas manquer les prochains articles.

Crédit : Photo de Pixabay

Licencié sous CC BY-NC-SA 4.0