Vue.js
Composables
Encapsuler et réutiliser de la logique réactive avec le pattern composable (useX)
Dans le chapitre précédent, on a écrit la logique fetch + isLoading + error directement dans HomePage. Dès qu'une autre page a besoin des films, on duplique tout. C'est là qu'interviennent les composables.
Qu'est-ce qu'un composable ?
Un composable est une fonction TypeScript qui encapsule et réutilise de la logique réactive. Le terme vient de la Composition API de Vue 3.
Un composable se distingue d'une fonction classique car il peut :
- Contenir des
ref,computed,watch - Écouter des hooks de cycle de vie (
onMounted,onUnmounted) - Être partagé entre plusieurs composants sans duplication de code
Par convention, les composables commencent par use : useShows, useApi, useLocalStorage…
useShows
On extrait la logique du HomePage du chapitre précédent dans un composable réutilisable.
Avant — logique dans le composant
<!-- src/pages/HomePage.vue -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { Show } from '@/types'
const BASE_URL = 'https://api.tvmaze.com'
const search = ref<string>('')
const shows = ref<Show[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
const seenShows = computed(() => shows.value.filter((m) => m.seen))
watch(search, (newVal) => { console.log('Recherche :', newVal) })
onMounted(async () => {
isLoading.value = true
try {
const res = await fetch(`${BASE_URL}/shows`)
if (!res.ok) throw new Error(`Erreur ${res.status}`)
const data: Omit<Show, 'seen'>[] = await res.json()
shows.value = data.slice(0, 12).map((m) => ({ ...m, seen: false }))
}
catch (e) {
error.value = e instanceof Error ? e.message : 'Erreur inconnue'
}
finally {
isLoading.value = false
}
})
const onToggleSeen = (show: Show) => {
show.seen = !show.seen
}
</script>
Après — logique extraite dans useShows
// src/composables/useShows.ts
import { ref, computed, onMounted } from 'vue'
import type { Show } from '@/types'
const BASE_URL = 'https://api.tvmaze.com'
export function useShows() {
const shows = ref<Show[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
const seenShows = computed(() => shows.value.filter((m) => m.seen))
onMounted(async () => {
isLoading.value = true
try {
const res = await fetch(`${BASE_URL}/shows`)
if (!res.ok) throw new Error(`Erreur ${res.status}`)
const data: Omit<Show, 'seen'>[] = await res.json()
shows.value = data.slice(0, 12).map((m) => ({ ...m, seen: false }))
}
catch (e) {
error.value = e instanceof Error ? e.message : 'Erreur inconnue'
}
finally {
isLoading.value = false
}
})
const toggleSeen = (show: Show) => {
show.seen = !show.seen
}
return { shows, seenShows, isLoading, error, toggleSeen }
}
<!-- src/pages/HomePage.vue -->
<template>
<div>
<p v-if="isLoading">Chargement...</p>
<p v-else-if="error">{{ error }}</p>
<ShowList v-else :shows="shows" @toggle-seen="toggleSeen">
<NSpace justify="space-between" align="center">
<NInput v-model:value="search" placeholder="Rechercher une série..."/>
<NTag type="success">{{ seenShows.length }} vu(s) sur {{ shows.length }}</NTag>
</NSpace>
</ShowList>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useShows } from '@/composables/useShows'
import ShowList from '../components/ShowList.component.vue'
const search = ref<string>('')
const { shows, seenShows, isLoading, error, toggleSeen } = useShows()
watch(search, (newVal) => {
console.log('Recherche :', newVal)
})
</script>