Composables: переиспользуемая логика
Composable — это функция, которая хранит своё реактивное состояние и логику, чтобы переиспользовать их в любом компоненте.
Суть: composable — функция с именем useXxx, инкапсулирующая реактивное состояние (ref, computed) и поведение. Она возвращает то, что нужно компоненту. В Nuxt composables из папки composables/ авто-импортируются и становятся стандартным способом делиться логикой.
Когда одна и та же логика нужна в нескольких компонентах — счётчик, форма, работа с корзиной — копировать её плохо. В Vue для этого придуманы composables: функции, которые упаковывают реактивное состояние и поведение и отдают их компоненту. Это замена старым миксинам: явная, типобезопасная, без скрытых конфликтов имён.
Composable — обычная функция, по соглашению с префиксом use. Внутри она создаёт реактивное состояние через ref или computed и возвращает то, что нужно снаружи:
// composables/useCounter.ts
export function useCounter(start = 0) {
const count = ref(start)
const double = computed(() => count.value * 2)
function inc() { count.value++ }
return { count, double, inc }
}
В компоненте это используется без импорта (спасибо авто-импорту Nuxt):
<script setup>
const { count, double, inc } = useCounter(10)
</script>
<template>
<button @click="inc">{{ count }} (×2 = {{ double }})</button>
</template>
Ключевая идея: composable инкапсулирует логику в замыкании. Состояние живёт внутри функции, наружу торчит только интерфейс. По-настоящему мощным это становится в связке с авто-импортом Nuxt — composables из папки composables/ доступны везде сами по себе.
Как работает под капотом
Composable опирается на систему реактивности Vue. ref создаёт реактивную ячейку: чтение через .value подписывает текущий компонент на изменения, запись — уведомляет всех подписчиков. computed кеширует производное значение и пересчитывает его только при изменении зависимостей. Composable просто группирует эти примитивы в переиспользуемую функцию-замыкание.
Важный нюанс: если composable создаёт состояние через локальный ref, каждый вызов даёт свой экземпляр состояния. Чтобы состояние было общим для всего приложения и переживало SSR, в Nuxt используют useState — об этом следующий урок. Смоделируем сам паттерн composable как замыкание, вручную реализовав мини-реактивность:
// Composable как замыкание с собственным состоянием.
function ref(initial) {
let value = initial;
const subs = [];
return {
get value() { return value; },
set value(v) { value = v; subs.forEach(fn => fn(v)); },
watch(fn) { subs.push(fn); },
};
}
function useCounter(start) {
const count = ref(start); // приватное состояние
const inc = () => { count.value++; };
return { count, inc }; // публичный интерфейс
}
const a = useCounter(10);
const b = useCounter(100); // независимый экземпляр
a.count.watch(v => console.log("a изменился ->", v));
a.inc(); a.inc();
console.log("a:", a.count.value, " b:", b.count.value);
Попробуй сам ▶ — два вызова useCounter дают независимые счётчики, а изменение одного запускает его watcher. Это и есть суть composable: своя капсула состояния плюс реактивность.
Частые ошибки
- Забыть
.value. В JS-коде сrefчитают и пишут через.value; в шаблоне Vue разворачивает его автоматически. - Ждать общего состояния от локального
ref. Каждый вызов composable создаёт свой ref. Для общего на всё приложение —useState. - Прятать в composable побочные эффекты без очистки. Таймеры и слушатели снимайте в
onScopeDispose/onUnmounted.
Best practices
- Имя — всегда
useXxx: это сигнал «здесь реактивная логика». - Возвращайте минимальный понятный интерфейс, прячьте детали внутри.
- Логику, общую для нескольких страниц, выносите в
composables/— это идиома Nuxt.
Итог: composables — это способ Vue и Nuxt делиться логикой без дублирования. Они хранят состояние в замыкании и отдают чистый интерфейс. Но локальное состояние не переживёт SSR и не станет общим — для этого есть useState, к которому мы переходим.
Ещё одна сильная сторона composables — тестируемость. Поскольку это обычная функция, возвращающая состояние и методы, её можно протестировать в изоляции, не монтируя ни одного компонента: вызвали, подёргали методы, проверили значения. Логика, размазанная по шаблонам и обработчикам компонента, тестируется куда тяжелее. Поэтому опытные команды сознательно вытягивают нетривиальную логику из компонентов в composables — не только ради переиспользования, но и ради того, чтобы её можно было покрыть быстрыми модульными тестами. Тонкий компонент и насыщенный composable — частый признак зрелой Nuxt-кодовой базы.