Vue 3 Composition API : Maîtrisez les Patterns Avancés pour des Applications Modernes
Introduction
La Composition API représente l'une des innovations les plus significatives de Vue 3, transformant radicalement notre approche du développement d'applications Vue.js. Cette nouvelle API ne se contente pas d'offrir une syntaxe alternative : elle ouvre la voie à des architectures plus modulaires, testables et maintenables.
Contrairement à l'Options API traditionnelle qui organise le code par type d'options (data, methods, computed...), la Composition API permet de regrouper la logique par fonctionnalité, créant ainsi des composants plus cohérents et une réutilisabilité accrue. Cette approche s'avère particulièrement puissante pour les applications complexes où la logique métier doit être partagée entre plusieurs composants.
Ce guide avancé vous accompagne dans la maîtrise de cette API révolutionnaire. Vous découvrirez les patterns sophistiqués qui distinguent les développeurs Vue.js experts, des composables réutilisables aux techniques d'optimisation, en passant par l'intégration TypeScript et les stratégies de test modernes.
Que vous souhaitiez migrer une application existante ou exploiter pleinement les capacités de Vue 3 dans un nouveau projet, ce guide vous fournit les connaissances approfondies nécessaires pour créer des applications Vue.js de niveau professionnel, robustes et évolutives.
Sommaire
- Au-delà des bases : Composables avancés
- Gestion d'état réactive avancée
- Techniques de performance avancées
- Patterns de composition avancés
- Composables utilitaires avancés
- Testing avec Composition API
- Migration depuis Options API
- Conclusion
Au-delà des bases : Composables avancés
Les composables permettent d'extraire et de réutiliser la logique stateful entre composants. Voici comment créer des composables sophistiqués :
// composables/useAsyncData.js
import { ref, computed, watchEffect } from "vue";
export function useAsyncData(fetchFunction) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const execute = async (...args) => {
loading.value = true;
error.value = null;
try {
data.value = await fetchFunction(...args);
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
const isReady = computed(() => !loading.value && !error.value && data.value);
const isEmpty = computed(
() => isReady.value && (!data.value || data.value.length === 0)
);
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
execute,
isReady,
isEmpty,
};
}
Gestion d'état réactive avancée
Pattern Store avec Composition API
// stores/userStore.js
import { reactive, computed, readonly } from "vue";
const state = reactive({
users: [],
currentUser: null,
filters: {
search: "",
status: "active",
page: 1,
limit: 10,
},
});
export const useUserStore = () => {
const filteredUsers = computed(() => {
let filtered = state.users.filter(
(user) =>
user.name.toLowerCase().includes(state.filters.search.toLowerCase()) &&
user.status === state.filters.status
);
// Pagination
const start = (state.filters.page - 1) * state.filters.limit;
return filtered.slice(start, start + state.filters.limit);
});
const totalPages = computed(() => {
const filtered = state.users.filter(
(user) =>
user.name.toLowerCase().includes(state.filters.search.toLowerCase()) &&
user.status === state.filters.status
);
return Math.ceil(filtered.length / state.filters.limit);
});
const addUser = (user) => {
state.users.push({
...user,
id: Date.now(),
createdAt: new Date(),
});
};
const updateUser = (id, updates) => {
const index = state.users.findIndex((user) => user.id === id);
if (index !== -1) {
state.users[index] = { ...state.users[index], ...updates };
}
};
const deleteUser = (id) => {
const index = state.users.findIndex((user) => user.id === id);
if (index !== -1) {
state.users.splice(index, 1);
}
};
const setCurrentUser = (user) => {
state.currentUser = user;
};
const updateFilter = (key, value) => {
state.filters[key] = value;
if (key !== "page") {
state.filters.page = 1; // Reset to first page on filter change
}
};
const resetFilters = () => {
state.filters = {
search: "",
status: "active",
page: 1,
limit: 10,
};
};
return {
// State (readonly pour éviter les mutations directes)
users: readonly(state.users),
currentUser: readonly(state.currentUser),
filters: readonly(state.filters),
// Getters
filteredUsers,
totalPages,
// Actions
addUser,
updateUser,
deleteUser,
setCurrentUser,
updateFilter,
resetFilters,
};
};
Techniques de performance avancées
Lazy loading avec Suspense
<template>
<div class="dashboard">
<h1>Dashboard</h1>
<Suspense>
<template #default>
<AsyncDashboardStats />
</template>
<template #fallback>
<div class="loading-skeleton">
<div class="skeleton-card"></div>
<div class="skeleton-chart"></div>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from "vue";
const AsyncDashboardStats = defineAsyncComponent({
loader: () => import("./components/DashboardStats.vue"),
delay: 200,
timeout: 10000,
errorComponent: () => h("div", "Erreur de chargement"),
});
</script>
Optimisation avec shallowRef et shallowReactive
// Pour des objets volumineux ou des structures complexes
import { shallowRef, shallowReactive, triggerRef } from "vue";
export function useLargeDataset() {
const dataset = shallowRef([]);
const metadata = shallowReactive({
totalItems: 0,
lastUpdated: null,
});
const updateDataset = (newData) => {
dataset.value = newData;
metadata.totalItems = newData.length;
metadata.lastUpdated = new Date();
// Déclencher manuellement la réactivité pour shallowRef
triggerRef(dataset);
};
return {
dataset,
metadata,
updateDataset,
};
}
Patterns de composition avancés
Injection de dépendances typée avec TypeScript
// types/injections.ts
export interface ApiService {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: any): Promise<T>;
}
// services/apiService.ts
export class HttpApiService implements ApiService {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
async post<T>(url: string, data: any): Promise<T> {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
}
}
// symbols.ts
import type { InjectionKey } from "vue";
import type { ApiService } from "./types/injections";
export const ApiServiceKey: InjectionKey<ApiService> = Symbol("ApiService");
// main.ts
import { ApiServiceKey } from "./symbols";
import { HttpApiService } from "./services/apiService";
app.provide(ApiServiceKey, new HttpApiService());
// Dans un composant
const apiService = inject(ApiServiceKey)!;
Custom directives réactives
// directives/vClickOutside.js
export const clickOutside = {
beforeMount(el, binding, vnode) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
// Mise à jour du handler si nécessaire
document.removeEventListener('click', el.clickOutsideEvent)
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
}
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}
// Usage dans un composant
<template>
<div v-click-outside="closeDropdown" class="dropdown">
<!-- Contenu du dropdown -->
</div>
</template>
Composables utilitaires avancés
useDebounce pour les recherches
// composables/useDebounce.js
import { ref, watch, computed } from "vue";
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value);
let timeoutId = null;
watch(value, (newValue) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
debouncedValue.value = newValue;
}, delay);
});
return debouncedValue;
}
// Usage
<script setup>
import {ref} from 'vue' import {useDebounce} from '@/composables/useDebounce'
const searchQuery = ref('') const debouncedSearchQuery =
useDebounce(searchQuery, 500) // La recherche ne se déclenche qu'après 500ms
d'inactivité watch(debouncedSearchQuery, (newQuery) =>{" "}
{performSearch(newQuery)})
</script>;
useLocalStorage avec synchronisation
// composables/useLocalStorage.js
import { ref, watch, Ref } from "vue";
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [Ref<T>, (value: T) => void] {
const storedValue = localStorage.getItem(key);
const parsedValue = storedValue ? JSON.parse(storedValue) : defaultValue;
const state = ref < T > parsedValue;
const setValue = (value: T) => {
state.value = value;
localStorage.setItem(key, JSON.stringify(value));
};
// Synchronisation avec les autres onglets
window.addEventListener("storage", (e) => {
if (e.key === key && e.newValue) {
state.value = JSON.parse(e.newValue);
}
});
// Sauvegarde automatique
watch(
state,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true }
);
return [state, setValue];
}
Testing avec Composition API
Test d'un composable
// composables/__tests__/useCounter.test.js
import { renderHook, act } from "@testing-library/vue-hooks";
import { useCounter } from "../useCounter";
describe("useCounter", () => {
it("should increment counter", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count.value).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count.value).toBe(1);
});
it("should decrement counter", () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count.value).toBe(4);
});
});
Migration depuis Options API
Comparaison des patterns
// Options API
export default {
data() {
return {
users: [],
loading: false
}
},
computed: {
activeUsers() {
return this.users.filter(user => user.active)
}
},
methods: {
async fetchUsers() {
this.loading = true
try {
this.users = await api.getUsers()
} finally {
this.loading = false
}
}
},
mounted() {
this.fetchUsers()
}
}
// Composition API équivalente
export default {
setup() {
const users = ref([])
const loading = ref(false)
const activeUsers = computed(() =>
users.value.filter(user => user.active)
)
const fetchUsers = async () => {
loading.value = true
try {
users.value = await api.getUsers()
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUsers()
})
return {
users,
loading,
activeUsers,
fetchUsers
}
}
}
Conclusion
La Composition API transforme Vue.js en un framework encore plus puissant et flexible pour les applications modernes. Elle excelle particulièrement pour :
Cas d'usage optimaux
- Logique complexe avec plusieurs sources de données réactives
- Réutilisation de la logique entre composants différents
- TypeScript avec une meilleure inférence de types
- Testing avec une logique plus facilement isolable
- Applications grandes avec une organisation modulaire
Bonnes pratiques à retenir
- Utilisez
readonly()pour exposer l'état en lecture seule - Préférez les composables pour la logique métier
- Combinez avec TypeScript pour une expérience développeur optimale
- Testez vos composables de manière isolée
- N'hésitez pas à mixer avec l'Options API selon le contexte
La Composition API n'est pas un remplacement forcé de l'Options API, mais une évolution naturelle qui ouvre de nouvelles possibilités architecturales passionnantes !