Если ты уже пишешь на 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, а не бизнес-логикой.
