Vue 3 Composition API: 5 паттернов, которые реально упрощают код компонентов

Если ты уже пишешь на Vue 3, но компоненты всё равно разрастаются и становятся нечитаемыми — скорее всего, ты используешь Composition API «в лоб». Ниже — 5 практических паттернов, которые помогают держать код компактным, переиспользуемым и предсказуемым.

1. Выносим логику в composables (не держим всё в компоненте)

Проблема:
компонент превращается в «помойку» из ref, watch, computed.

Решение:
любую бизнес-логику — в composable.

Было

<script setup>
import { ref, onMounted } from 'vue'const users = ref([])
const loading = ref(false)async function fetchUsers() {
loading.value = true
const res = await fetch('/api/users')
users.value = await res.json()
loading.value = false
}onMounted(fetchUsers)
</script>

Стало

// composables/useUsers.ts
import { ref } from 'vue'export function useUsers() {
const users = ref([])
const loading = ref(false) async function fetchUsers() {
loading.value = true
const res = await fetch('/api/users')
users.value = await res.json()
loading.value = false
} return { users, loading, fetchUsers }
}
<script setup>
import { onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'const { users, loading, fetchUsers } = useUsers()onMounted(fetchUsers)
</script>

Профит:

  • код переиспользуется
  • компонент читается за 10 секунд
  • тестировать проще

2. Разделяем состояние и эффекты

Проблема:
перемешаны данные, вычисления и побочные эффекты (watch, API).

Решение:
держим state отдельно от эффектов.

Паттерн

// composables/useCounter.ts
import { ref, computed, watch } from 'vue'export function useCounter() {
// state
const count = ref(0) // derived state
const double = computed(() => count.value * 2) // effects
watch(count, (val) => {
console.log('count changed:', val)
}) function increment() {
count.value++
} return { count, double, increment }
}

Важно:

  • ref → данные
  • computed → производные
  • watch → side effects

Профит:

  • меньше багов
  • легче дебажить
  • понятная структура

3. Паттерн «factory composable» (параметризуем логику)

Проблема:
один и тот же composable нужен с разными параметрами.

Решение:
делаем фабрику.

Пример: универсальный fetch

// composables/useFetch.ts
import { ref } from 'vue'export function useFetch(url: string) {
const data = ref(null)
const loading = ref(false)
const error = ref(null) async function execute() {
loading.value = true
error.value = null try {
const res = await fetch(url)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
} return { data, loading, error, execute }
}

Использование

const { data: users, execute } = useFetch('/api/users')
const { data: posts, execute: loadPosts } = useFetch('/api/posts')

Профит:

  • один composable → много кейсов
  • нет дублирования
  • гибкость

«Expose API» вместо хаоса в return

Проблема:
в composable возвращается 10+ переменных → непонятно, что важно.

Решение:
структурируем API.

Было

return {
count,
double,
increment,
reset,
isEven,
lastUpdated,
}

Стало

return {
state: {
count,
double,
isEven,
},
actions: {
increment,
reset,
},
meta: {
lastUpdated,
},
}

Использование

const { state, actions } = useCounter()actions.increment()
console.log(state.count.value)

Профит:

  • сразу видно структуру
  • проще масштабировать
  • меньше «магии»

Паттерн «controlled vs uncontrolled state»

Очень полезен для компонентов UI (инпуты, модалки, селекты).

Проблема:
компонент должен работать и как controlled, и как standalone.

Решение

// composables/useControlled.ts
import { ref, computed } from 'vue'export function useControlled(props, emit, name: string) {
const internal = ref(props[name]) const value = computed({
get: () => props[name] ?? internal.value,
set: (val) => {
internal.value = val
emit(`update:${name}`, val)
},
}) return value
}

Использование

<script setup>
const props = defineProps({
modelValue: String,
})

const emit = defineEmits(['update:modelValue'])

const value = useControlled(props, emit, 'modelValue')
</script>

Профит:

  • компонент гибкий
  • работает и с v-model, и без него
  • нет дублирования логики

Бонус: не злоупотребляй watch

Частая ошибка — писать watch там, где нужен computed.

Плохо

const fullName = ref('')watch([firstName, lastName], () => {
fullName.value = `${firstName.value} ${lastName.value}`
})

Хорошо

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

Правило:

  • если можно выразить через computed → делай так
  • watch только для сайд-эффектов (API, localStorage, логирование)

Итог

Composition API сам по себе не делает код лучше — он просто даёт инструменты.
Чистота начинается с архитектуры.

Если кратко:

  • выноси логику в composables
  • разделяй state / derived / effects
  • делай composables параметризуемыми
  • структурируй return API
  • думай про controlled state

И главное — не пиши всё в одном компоненте.
Компонент должен быть декларацией UI, а не бизнес-логикой.