Massimo Russo
Tu as appris les Composition API. Tu as lu la doc. Tu as même migré quelques composants. Et pourtant, tu utilises les composables comme tu utilisais les mixins il y a 5 ans : sans vraiment comprendre pourquoi.
Le résultat ? Des composables que tu partages entre 2 fichiers, mais qui restent pleins d'effets de bord. Des partages de données qui se cachent sous une couche d'abstraction. De la prop drilling déguisée en composable.
Voilà le truc : les composables ne sont pas juste une syntaxe plus propre pour partager du code. C'est une philosophie architecturale qui force à penser différemment.
Et 90% des devs la ratent.
Avant les Composition API, tu avais un choix binaire : props ou state global.
Props ? Ça scalait mal. Prop drilling jusqu'à l'infini.
State global ? Tu mettais tout dans Vuex et tu créais un monolithe impossible à déboguer.
Les composables, eux, offrent une troisième voie. Mais seulement si tu comprends ce qu'ils sont réellement.
Un composable, c'est pas :
Un composable, c'est une capsule de logique métier réutilisable, isolée, testable.
La différence ? Énorme.
Tu vois ce pattern constamment :
// ❌ Ceci n'est PAS un composable
const useFetchWithCaching = () => {
const cache = ref({});
const data = ref(null);
const loading = ref(false);
const fetchData = async (url) => {
if (cache.value[url]) {
data.value = cache.value[url];
return;
}
loading.value = true;
const res = await fetch(url);
data.value = await res.json();
cache.value[url] = data.value;
loading.value = false;
};
return { data, loading, fetchData };
};
C'est du code partagé. C'est pas un composable. Pourquoi ?
Effets de bord cachés : Le cache est global à ce composable. Si tu utilises ce composable dans 2 composants différents, ils partagent le même cache. C'est une bombe à retardement.
Responsabilités mélangées : Fetch, caching, state... c'est 3 trucs distincts. Mais tu les as fusionnés.
Pas testable en isolation : Comment tu testes le caching sans monter un composant ? Comment tu testes le fetch sans le cache ?
Sépare les responsabilités :
// ✅ Composables isolés, chacun avec une responsabilité
const useCache = () => {
const cache = ref({});
return {
get: (key) => cache.value[key],
set: (key, value) => { cache.value[key] = value; },
has: (key) => key in cache.value
};
};
const useFetch = (url) => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetch = async () => {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
};
onMounted(fetch);
return { data, loading, error, fetch };
};
// Dans le composant, tu composes les deux
const useSmartFetch = (url) => {
const cache = useCache();
const fetch = useFetch(url);
if (cache.has(url)) {
fetch.data.value = cache.get(url);
} else {
watch(() => fetch.data.value, (newData) => {
if (newData) cache.set(url, newData);
});
}
return { ...fetch };
};
Maintenant, tu peux :
useCache indépendammentuseFetch indépendammentuseCache ailleurs sans crainteC'est ça, la vraie composition.
Voilà un anti-pattern qu'on voit BEAUCOUP :
// ❌ Logique métier dans le composant
<template>
<div>
<input v-model="email" />
<input v-model="password" />
<button @click="handleLogin">Login</button>
</div>
</template>
<script setup>
const email = ref('');
const password = ref('');
const loading = ref(false);
const error = ref(null);
const handleLogin = async () => {
if (!email.value.includes('@')) {
error.value = 'Invalid email';
return;
}
if (password.value.length < 8) {
error.value = 'Password too short';
return;
}
loading.value = true;
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: email.value, password: password.value })
});
if (!res.ok) throw new Error('Login failed');
// redirect ou update store
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
};
</script>
C'est un cauchemar à tester. Tu veux tester la validation ? Tu dois monter le composant. Tu veux tester le fetch ? Encore le composant. Tu veux réutiliser la logique ailleurs ? Copie-colle.
Extrais TOUTE la logique métier :
// ✅ Logique métier dans un composable
const useLogin = () => {
const email = ref('');
const password = ref('');
const loading = ref(false);
const error = ref(null);
const validateEmail = () => email.value.includes('@');
const validatePassword = () => password.value.length >= 8;
const login = async () => {
error.value = null;
if (!validateEmail()) {
error.value = 'Invalid email';
return false;
}
if (!validatePassword()) {
error.value = 'Password too short';
return false;
}
loading.value = true;
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: email.value, password: password.value })
});
if (!res.ok) throw new Error('Login failed');
return true;
} catch (e) {
error.value = e.message;
return false;
} finally {
loading.value = false;
}
};
return {
email,
password,
loading,
error,
validateEmail,
validatePassword,
login
};
};
// Le composant devient JUSTE une UI
<template>
<div>
<input v-model="email" />
<input v-model="password" />
<p v-if="error" class="error">{{ error }}</p>
<button @click="login" :disabled="loading">
{{ loading ? 'Logging in...' : 'Login' }}
</button>
</div>
</template>
<script setup>
const { email, password, loading, error, login } = useLogin();
</script>
Maintenant :
useLogin sans composantuseLogin dans un formulaire, un dialog, un API clientVoilà une croyance dangereuse : "Avec les composables, j'ai plus besoin de Pinia!"
Faux.
Les composables et Pinia font deux choses différentes.
Composables : Logique réutilisable, comportement, logique métier. Scope : local au composant ou à quelques composants.
Pinia : Source unique de vérité pour l'état applicatif. Scope : global, persistable, observable across the app.
Exemple :
// ✅ Bon séparation des préoccupations
// useLogin c'est de la logique métier locale
const useLogin = () => {
const email = ref('');
const password = ref('');
// ... validation, fetch
return { email, password, login };
};
// useAuth c'est l'état global
const useAuthStore = defineStore('auth', () => {
const user = ref(null);
const isAuthenticated = computed(() => !!user.value);
const setUser = (newUser) => { user.value = newUser; };
return { user, isAuthenticated, setUser };
});
// Dans le composant, tu utilises les deux
const LoginComponent = {
setup() {
const { email, password, login } = useLogin();
const authStore = useAuthStore();
const handleLogin = async () => {
if (await login()) {
// Fetch successful, maintenant update le state global
const userData = await fetchUserProfile();
authStore.setUser(userData);
}
};
return { email, password, handleLogin };
}
};
Le composable c'est "comment on se login". Le store c'est "qui est logué".
Pour maîtriser les composables, tu dois intégrer ce modèle :
// ✅ Bon : chaque composable fait une chose
const useFormValidation = () => { /* ... */ };
const useFetch = (url) => { /* ... */ };
const useDebounce = (value, delay) => { /* ... */ };
const useLocalStorage = (key) => { /* ... */ };
// ❌ Mauvais : effet de bord caché
const useUser = () => {
const user = ref(null);
// Ce fetch s'exécute à chaque call du composable
// Et tu ne le vois pas
fetch('/api/user').then(r => r.json()).then(u => user.value = u);
return { user };
};
// ✅ Bon : effet de bord explicite
const useUser = () => {
const user = ref(null);
const fetchUser = async () => {
const res = await fetch('/api/user');
user.value = await res.json();
};
// C'est au composant de décider quand fetchUser s'exécute
onMounted(fetchUser);
return { user, fetchUser };
};
// ❌ Pas de "super composable"
const useSuperComposable = () => {
const { data, loading } = useFetch(url);
const { cache } = useCache();
const { validation } = useValidation();
// ... 20 autres trucs
return { data, loading, cache, validation, ... };
};
// ✅ Bon : compose les petits trucs dans le composant
const MyComponent = {
setup() {
const fetch = useFetch(url);
const cache = useCache();
const validation = useValidation();
// Logique pour combiner les trois
if (cache.has(url)) {
fetch.data.value = cache.get(url);
}
return { fetch, cache, validation };
}
};
Avant de conclure, soyons honnêtes sur les arbitrages :
✅ Modularité réelle ✅ Réutilisabilité sans copie-colle ✅ Testabilité en isolation ✅ Composition flexible ✅ Tree-shakeable (meilleur bundle)
❌ Plus de "boilerplate" mental (tu dois bien structurer) ❌ Debugging plus complexe (plus de couches) ❌ Courbe d'apprentissage (besoin de comprendre la composition) ❌ Peut devenir trop abstrait si mal utilisé
Avant de partager un composable, pose-toi :
Si tu réponds "oui" aux 5, c'est un bon composable.
Les composables sont la fondation de l'architecture Vue moderne. Mais seulement si tu comprends qu'ils ne sont pas une solution magique.
C'est un outil qui force la clarté si tu l'utilises bien.
La prochaine fois que tu écris un composable, demande-toi : "Est-ce que je crée vraiment une capsule de logique réutilisable et testable ? Ou est-ce que je juste partage du code pour éviter la prop drilling ?"
Si c'est la deuxième réponse, tu fais pas des composables. Tu fais du refactoring cosmétique.
Les vraies architectes ne maîtrisent pas les composables. Ils comprennent pourquoi les composables existent.