Стандарты

Composables

Composables — переиспользуемые функции с реактивным состоянием.

Composables — переиспользуемые функции с реактивным состоянием.

Когда создавать composable

СитуацияСоздать composable
Логика используется в 2+ компонентах
API запросы для сущности
Сложная логика в компоненте (>50 строк)
Работа с внешним сервисом (Ably, etc.)
Простой утилитарный код❌ (→ utils/)

Структура composables/

app/composables/
├── useApi.ts              # HTTP клиент
├── useSession.ts          # Сессия пользователя
├── useTableFilters.ts     # Пагинация и фильтры
│
├── useOrders.ts           # API заказов
├── useUsers.ts            # API пользователей
├── useServices.ts         # API услуг
├── useCompanies.ts        # API компаний
│
├── useAbly.ts             # Realtime
└── useMobile.ts           # Responsive

Паттерн composable

Базовая структура

// app/composables/useOrders.ts
export function useOrders() {
  // Зависимости
  const api = useApi()

  // Реактивное состояние (если нужно)
  const orders = ref<Order[]>([])
  const loading = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const hasOrders = computed(() => orders.value.length > 0)

  // Методы
  async function fetchOrders(params?: OrderParams) {
    loading.value = true
    error.value = null
    try {
      const response = await api.get<PaginatedResponse<Order>>(
        `/orders?${toQueryParams(params)}`
      )
      orders.value = response.data
      return response
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }

  async function createOrder(data: CreateOrderDto) {
    return api.post<Order>('/orders', data)
  }

  async function updateOrder(id: string, data: UpdateOrderDto) {
    return api.patch<Order>(`/orders/${id}`, data)
  }

  async function deleteOrder(id: string) {
    return api.delete(`/orders/${id}`)
  }

  // Return
  return {
    // State
    orders,
    loading,
    error,
    // Computed
    hasOrders,
    // Methods
    fetchOrders,
    createOrder,
    updateOrder,
    deleteOrder,
  }
}

Типизация

TypeScript автоматически выводит типы возврата из composable. Не нужно создавать отдельный интерфейс:

// ❌ Избыточно — интерфейс дублирует то, что TS выведет сам
interface UseOrdersReturn {
  orders: Ref<Order[]>
  loading: Ref<boolean>
  error: Ref<Error | null>
  // ...
}
export function useOrders(): UseOrdersReturn { ... }

// ✅ Хорошо — тип возврата выводится автоматически
export function useOrders() {
  const orders = ref<Order[]>([])     // Тип нужен — пустой массив
  const loading = ref(false)           // Тип выводится
  const error = ref<Error | null>(null) // Тип нужен — nullable

  return { orders, loading, error }
  // TypeScript выведет: { orders: Ref<Order[]>, loading: Ref<boolean>, ... }
}

См. standard-typescript — правило 6.

Использование в компонентах

<script setup lang="ts">
// Composable автоматически импортируется
const { orders, loading, fetchOrders, createOrder } = useOrders()

// Фетч при монтировании
onMounted(() => {
  fetchOrders({ page: 1, status: 'active' })
})

// Создание заказа
async function handleSubmit(data: CreateOrderDto) {
  await createOrder(data)
  await fetchOrders()  // Рефетч
}
</script>

<template>
  <div v-if="loading">Загрузка...</div>
  <OrderTable v-else :data="orders" />
</template>

Правила

1. Имя начинается с use

// ✅ Хорошо
useOrders()
useApi()
useTableFilters()

// ❌ Плохо
orders()
apiClient()
tableFilters()

2. Возвращай объект, не массив

// ✅ Хорошо — объект с именованными свойствами
return { orders, loading, fetchOrders }

// ❌ Плохо — массив (сложно запомнить порядок)
return [orders, loading, fetchOrders]

3. Не храни состояние между вызовами (если не нужен singleton)

// Каждый вызов создаёт новое состояние
export function useOrders() {
  const orders = ref([])  // Новый ref при каждом вызове
  return { orders }
}

// Singleton — состояние общее
const orders = ref([])  // Вне функции
export function useOrdersSingleton() {
  return { orders }
}

4. Выноси бизнес-логику из компонентов

<!-- ❌ Плохо — логика в компоненте -->
<script setup>
const api = useApi()
const orders = ref([])

async function fetchOrders() {
  orders.value = await api.get('/orders')
}

async function createOrder(data) {
  await api.post('/orders', data)
  await fetchOrders()
}
</script>

<!-- ✅ Хорошо — логика в composable -->
<script setup>
const { orders, fetchOrders, createOrder } = useOrders()
onMounted(fetchOrders)
</script>

Чек-лист

  • Имя начинается с use
  • Возвращает объект
  • Типизированы параметры и API ответы (возврат выводится автоматически)
  • Не содержит UI логику
  • Документирован JSDoc (если сложный)

Связанные стандарты