πŸ”„ Qu'est-ce qu'un composable ?

Un composable est une fonction JavaScript qui utilise la Composition API de Vue pour encapsuler et rΓ©utiliser de la logique stateful (avec Γ©tat) entre plusieurs composants.

La situation sans composable

// ❌ Duplication : mΓͺme logique dans deux composants
const ComponentA = {
  setup() {
    const x = ref(0);
    const y = ref(0);
    function update(event) { x.value = event.pageX; y.value = event.pageY; }
    window.addEventListener('mousemove', update);
    onUnmounted(() => window.removeEventListener('mousemove', update));
    return { x, y };
  }
};

const ComponentB = {
  setup() {
    // Exactement la mΓͺme logique copiΓ©e-collΓ©e...
    const x = ref(0);
    const y = ref(0);
    // ...
  }
};

Avec un composable

// βœ… Logique extraite dans un composable rΓ©utilisable
function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function update(event) {
    x.value = event.pageX;
    y.value = event.pageY;
  }

  onMounted(()  => window.addEventListener('mousemove', update));
  onUnmounted(() => window.removeEventListener('mousemove', update));

  return { x, y };  // Retourner les refs pour que l'appelant reste rΓ©actif
}

// Utilisation dans n'importe quel composant
const ComponentA = {
  setup() {
    const { x, y } = useMouse();  // Propre, rΓ©utilisable
    return { x, y };
  }
};

Composables vs Mixins (Vue 2)

ComposablesMixins Vue 2
Pas de conflits de noms β€” on nomme ce qu'on reΓ§oitConflits possibles si deux mixins ont la mΓͺme prop
Source des donnΓ©es expliciteOrigine obscure (magic props)
ParamΓ©trable facilementConfiguration globale laborieuse
Compatible TypeScriptTypage difficile
πŸ’‘ Les composables sont l'une des meilleures fonctionnalitΓ©s de Vue 3. Ils permettent de partager n'importe quelle logique rΓ©active β€” timer, fetch, Γ©vΓ©nements, formulaire β€” sous forme de fonctions pures et testables.

πŸ“ Conventions des composables

Règles à respecter

// βœ… 1. Nommer avec le prΓ©fixe "use"
function useToggle() { /* ... */ }
function useLocalStorage() { /* ... */ }
function useFetch() { /* ... */ }

// βœ… 2. Retourner des refs et des fonctions (pas des valeurs brutes)
function useCounter() {
  const count = ref(0);
  function increment() { count.value++; }
  return { count, increment };  // count est une ref — reste réactif après déstructuration
}

// βœ… 3. Accepter des paramΓ¨tres rΓ©actifs (ref ou computed)
function useDouble(n) {
  // Accepter n comme ref ou valeur brute grΓ’ce Γ  toValue()
  const { toValue } = Vue;
  return computed(() => toValue(n) * 2);
}

// ❌ 4. Ne PAS appeler dans des if, for, ou setTimeout
function setup() {
  if (condition) {
    const { x } = useMouse(); // ❌ Interdit β€” doit Γͺtre au niveau racine
  }

  // βœ… Correct
  const { x } = useMouse();
  // Ensuite utiliser x dans un computed ou watch conditionnel
}

Structure type d'un composable

const { ref, onMounted, onUnmounted, watch } = Vue;

function useXxx(param) {
  // 1. State local au composable
  const data    = ref(null);
  const loading = ref(false);
  const error   = ref(null);

  // 2. Logique (watchers, lifecycle hooks, etc.)
  onMounted(() => {
    // Initialisation
  });

  onUnmounted(() => {
    // Nettoyage β€” crucial pour Γ©viter les fuites mΓ©moire !
  });

  // 3. Fonctions exposΓ©es
  function faire() { /* ... */ }

  // 4. Retourner l'API publique du composable
  return { data, loading, error, faire };
}

πŸ’Ύ useLocalStorage

Synchroniser automatiquement une ref avec localStorage : lecture au dΓ©marrage, sauvegarde Γ  chaque changement.

const { ref, watch } = Vue;

function useLocalStorage(key, valeurDefaut) {
  // Initialiser depuis localStorage ou avec la valeur par dΓ©faut
  function lire() {
    try {
      const item = localStorage.getItem(key);
      return item !== null ? JSON.parse(item) : valeurDefaut;
    } catch {
      return valeurDefaut;
    }
  }

  const data = ref(lire());

  // Sauvegarder automatiquement Γ  chaque changement
  watch(data, (nouvelleValeur) => {
    try {
      localStorage.setItem(key, JSON.stringify(nouvelleValeur));
    } catch {}
  }, { deep: true }); // deep: true pour les objets/tableaux

  return data;  // Retourner la ref directement (pas un objet)
}

// Utilisation
const compteur = useLocalStorage('compteur', 0);
const prefs    = useLocalStorage('prefs', { theme: 'dark', langue: 'fr' });

// Modifier : synchronisΓ© automatiquement avec localStorage
compteur.value++;
prefs.value.theme = 'light';

🌐 useFetch

Encapsuler une requΓͺte HTTP avec gestion automatique de loading et error.

const { ref, watch, toValue, isRef } = Vue;

function useFetch(url) {
  const data    = ref(null);
  const loading = ref(false);
  const error   = ref(null);

  async function fetchData() {
    // RΓ©soudre l'URL (peut Γͺtre une string, une ref, ou un computed)
    const urlValue = isRef(url) ? url.value : url;
    if (!urlValue) return;

    loading.value = true;
    error.value   = null;
    data.value    = null;

    try {
      const response = await fetch(urlValue);
      if (!response.ok) throw new Error('HTTP ' + response.status);
      data.value = await response.json();
    } catch (err) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  }

  // Refetch si l'URL change (utile si url est une ref)
  if (isRef(url)) {
    watch(url, fetchData, { immediate: true });
  } else {
    fetchData();
  }

  return { data, loading, error, refetch: fetchData };
}

// Utilisation
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

// Avec URL rΓ©active
const todoId = ref(1);
const url = computed(() => \`https://jsonplaceholder.typicode.com/todos/\${todoId.value}\`);
const { data: todo } = useFetch(url); // Refetch automatiquement quand todoId change

⏱️ useDebounce

Retarder la propagation d'une valeur pour Γ©viter des appels trop frΓ©quents (saisie dans un champ de recherche, resize...).

const { ref, watch } = Vue;

function useDebounce(source, delai = 300) {
  const debounced = ref(typeof source === 'object' ? source.value : source);
  let timer = null;

  watch(
    () => (source && typeof source === 'object' ? source.value : source),
    (nouvelleValeur) => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        debounced.value = nouvelleValeur;
      }, delai);
    }
  );

  return debounced;
}

// Utilisation classique β€” champ de recherche
const recherche   = ref('');            // Valeur immΓ©diate (liΓ©e Γ  l'input)
const rechercheDB = useDebounce(recherche, 400); // Valeur retardΓ©e de 400ms

// rechercheDB ne change qu'après 400ms d'inactivité
watch(rechercheDB, (valeur) => {
  // API appelΓ©e seulement quand l'utilisateur s'arrΓͺte de taper
  fetchResultats(valeur);
});
βœ… Sans debounce, taper "Vue.js" dans un champ de recherche dΓ©clencherait 6 appels API (v, vu, vue, vue., vue.j, vue.js). Avec debounce Γ  300ms, un seul appel est fait.

🎯 useEventListener

Ajouter et retirer des Γ©couteurs d'Γ©vΓ©nements proprement, avec nettoyage automatique quand le composant est dΓ©montΓ©.

const { onMounted, onUnmounted, isRef } = Vue;

function useEventListener(cible, evenement, handler) {
  // cible peut Γͺtre window, document, un ref d'Γ©lΓ©ment...
  function getCible() {
    return isRef(cible) ? cible.value : cible;
  }

  onMounted(() => {
    getCible()?.addEventListener(evenement, handler);
  });

  onUnmounted(() => {
    // Nettoyage obligatoire pour Γ©viter les fuites mΓ©moire
    getCible()?.removeEventListener(evenement, handler);
  });
}

// ─── Exemples d'utilisation ───

// DΓ©tecter la touche Escape
function useKeyPress(touche) {
  const appuyee = ref(false);
  useEventListener(window, 'keydown', (e) => {
    appuyee.value = (e.key === touche);
  });
  useEventListener(window, 'keyup', (e) => {
    if (e.key === touche) appuyee.value = false;
  });
  return appuyee;
}

// Taille de la fenΓͺtre rΓ©active
function useWindowSize() {
  const width  = ref(window.innerWidth);
  const height = ref(window.innerHeight);
  useEventListener(window, 'resize', () => {
    width.value  = window.innerWidth;
    height.value = window.innerHeight;
  });
  return { width, height };
}
⚠️ Sans onUnmounted, les Γ©vΓ©nements restent actifs mΓͺme aprΓ¨s la destruction du composant. Cela provoque des fuites mΓ©moire et peut dΓ©clencher des erreurs sur des composants dΓ©jΓ  supprimΓ©s.
✏️ Exercices V09 β–Ά Mini-projet Dashboard Suivant : Lifecycle β†’