π 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)
| Composables | Mixins Vue 2 |
|---|---|
| Pas de conflits de noms β on nomme ce qu'on reΓ§oit | Conflits possibles si deux mixins ont la mΓͺme prop |
| Source des donnΓ©es explicite | Origine obscure (magic props) |
| ParamΓ©trable facilement | Configuration globale laborieuse |
| Compatible TypeScript | Typage 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.