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-кодовой базы.

Проверьте себя
1. Что такое composable в Vue/Nuxt?
AГотовый UI-компонент
BФункция с префиксом use, инкапсулирующая реактивное состояние и логику для переиспользования
CФайл конфигурации сборки
DСерверный API-обработчик
2. Что вернут два отдельных вызова useCounter(), если состояние внутри создано через локальный ref?
AОдин общий счётчик на оба вызова
BДва независимых экземпляра состояния
CОшибку дублирования
DСостояние с сервера