← Retour à la liste

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

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 !