Антон Ларичев

Введение
Vue 3 Composition API кардинально изменил подход к организации логики в компонентах. Вместо разбросанных по Options API свойств data, methods и computed, теперь можно группировать связанную логику в composable-функции и переиспользовать её между компонентами.
В этой статье разберём 5 реальных паттернов Vue 3 Composition API, которые применяются в продакшен-проектах. Каждый паттерн содержит готовый пример на TypeScript, который можно адаптировать под свои задачи. Если вы уже используете <script setup> и хотите писать чище и эффективнее, эти composables будут полезны.
Паттерн 1: useAsync для загрузки данных
Самый частый сценарий в любом приложении — запрос данных с сервера. Вместо копирования логики loading, error, data в каждый компонент, выносим всё в composable.
import { ref, type Ref } from 'vue'
interface UseAsyncReturn<T> {
data: Ref<T | null>
error: Ref<string | null>
loading: Ref<boolean>
execute: () => Promise<void>
}
export function useAsync<T>(asyncFn: () => Promise<T>): UseAsyncReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
data.value = await asyncFn()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Неизвестная ошибка'
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
Как использовать useAsync в компоненте
<script setup lang="ts">
import { useAsync } from '@/composables/useAsync'
import { fetchUsers } from '@/api/users'
// Вызываем composable и сразу запускаем загрузку
const { data: users, loading, error, execute } = useAsync(fetchUsers)
execute()
</script>
<template>
<div v-if="loading">Загрузка...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
Теперь логика загрузки данных инкапсулирована: в компоненте остаётся только вызов и шаблон.
Паттерн 2: useFormField с валидацией через computed
Формы — второй по частоте сценарий, где код быстро превращается в кашу. Composable useFormField связывает значение поля с правилами валидации через computed.
import { ref, computed } from 'vue'
type ValidationRule = (value: string) => string | true
export function useFormField(initialValue: string, rules: ValidationRule[] = []) {
const value = ref(initialValue)
const touched = ref(false)
// Реактивно вычисляем ошибку при каждом изменении value
const error = computed(() => {
if (!touched.value) return null
for (const rule of rules) {
const result = rule(value.value)
if (result !== true) return result
}
return null
})
const isValid = computed(() => error.value === null && touched.value)
const blur = () => { touched.value = true }
const reset = () => {
value.value = initialValue
touched.value = false
}
return { value, error, isValid, touched, blur, reset }
}
Использование в компоненте:
<script setup lang="ts">
import { useFormField } from '@/composables/useFormField'
const email = useFormField('', [
(v) => v.length > 0 || 'Обязательное поле',
(v) => v.includes('@') || 'Некорректный email',
])
</script>
<template>
<input v-model="email.value" @blur="email.blur" />
<span v-if="email.error">{{ email.error }}</span>
</template>
Каждое поле формы становится самостоятельным реактивным объектом. Валидация срабатывает автоматически при изменении значения.
Паттерн 3: readonly-состояние через provide/inject
Когда нескольким компонентам нужен доступ к общему состоянию, часто возникает соблазн использовать глобальный store. Но для локальных задач хватает связки provide, inject и readonly.
import { ref, provide, inject, readonly, type InjectionKey, type Ref } from 'vue'
interface NotificationState {
message: Ref<string>
show: (msg: string) => void
hide: () => void
}
const NotificationKey: InjectionKey<NotificationState> = Symbol('notification')
// Вызывается в корневом компоненте
export function provideNotification() {
const message = ref('')
const show = (msg: string) => { message.value = msg }
const hide = () => { message.value = '' }
// Потребители получают readonly-версию состояния
provide(NotificationKey, {
message: readonly(message) as Ref<string>,
show,
hide,
})
}
// Вызывается в дочерних компонентах
export function useNotification(): NotificationState {
const state = inject(NotificationKey)
if (!state) throw new Error('provideNotification не вызван')
return state
}
Этот паттерн Vue 3 Composition API позволяет избежать props drilling и при этом сохранить контроль над мутациями: дочерние компоненты могут вызывать show и hide, но не могут напрямую менять message.
Паттерн 4: useDebounce для отложенных вычислений
Поиск с автодополнением, фильтрация списков, автосохранение — все эти задачи требуют debounce. Composable с watch решает это элегантно.
import { ref, watch, type Ref } from 'vue'
export function useDebounce<T>(source: Ref<T>, delay = 300): Ref<T> {
const debounced = ref(source.value) as Ref<T>
let timeout: ReturnType<typeof setTimeout>
watch(source, (newValue) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debounced.value = newValue
}, delay)
})
return debounced
}
Пример: поиск с debounce во Vue 3
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
import { useAsync } from '@/composables/useAsync'
import { searchProducts } from '@/api/products'
const query = ref('')
const debouncedQuery = useDebounce(query, 400)
const { data: results, execute } = useAsync(() => searchProducts(debouncedQuery.value))
// Запускаем поиск при изменении отложенного значения
watch(debouncedQuery, () => {
if (debouncedQuery.value.length >= 2) execute()
})
</script>
<template>
<input v-model="query" placeholder="Поиск товаров..." />
<ul v-if="results">
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
</template>
Обратите внимание, как composables комбинируются: useDebounce и useAsync работают вместе, при этом каждый отвечает за свою задачу.
Паттерн 5: composable-синглтон для глобального состояния
Иногда нужно глобальное реактивное состояние без тяжёлых стейт-менеджеров. Если вынести ref за пределы функции, все вызовы composable будут ссылаться на один и тот же экземпляр.
import { ref, computed } from 'vue'
// Состояние создаётся один раз при импорте модуля
const theme = ref<'light' | 'dark'>('light')
export function useTheme() {
const isDark = computed(() => theme.value === 'dark')
const toggle = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme.value)
}
const set = (value: 'light' | 'dark') => {
theme.value = value
document.documentElement.setAttribute('data-theme', value)
}
return { theme, isDark, toggle, set }
}
В любом компоненте вызов useTheme() вернёт ссылку на один и тот же реактивный theme. Это паттерн синглтона на уровне модуля, который работает без дополнительных зависимостей.
Частые ошибки при работе с Composition API
При создании composables разработчики допускают типичные ошибки:
Потеря реактивности при деструктуризации. Если composable возвращает reactive-объект, деструктуризация потеряет реактивность. Возвращайте объект с ref-значениями или используйте toRefs.
// Плохо: реактивность потеряна
const { count } = useCounter() // count — обычное число
// Хорошо: реактивность сохранена
const { count } = useCounter() // count — Ref<number>
Вызов composables вне setup. Composables, использующие onMounted, watch или provide, должны вызываться внутри <script setup> или функции setup(). Иначе Vue не сможет привязать хуки жизненного цикла к текущему экземпляру компонента.
Слишком крупные composables. Composable на 200 строк — это тот же монолит, только в другой обёртке. Разбивайте на мелкие функции и комбинируйте их.
Заключение
Паттерны Vue 3 Composition API — это не абстрактная теория, а инструменты для ежедневной работы. Composables useAsync, useFormField, useDebounce, useTheme и provide/inject с readonly покрывают большинство типовых задач. Главное правило — каждый composable решает одну задачу, а сложное поведение собирается из простых кирпичиков. Используйте ref для примитивов, computed для производных значений, watch для побочных эффектов, и ваш код останется чистым и поддерживаемым.



Комментарии
0