Просунуті Vue 3 Composables: патерни повторного використання та питання для співбесіди 2026
Вичерпний посібник з просунутих Vue 3 Composables: патерни повторного використання, асинхронна обробка помилок, Dependency Injection, валідація форм та актуальні питання для технічних співбесід у 2026 році.

Vue 3 Composables закріпили за собою статус основного механізму виділення та повторного використання реактивної логіки між компонентами. З появою Alien Signals та сумісністю з Vapor Mode у Vue 3.6 майстерне володіння патернами Composable безпосередньо впливає на архітектуру застосунків, продуктивність та підтримуваність коду у 2026 році.
Composition API докорінно змінило підхід до організації та повторного використання логіки у Vue-застосунках. В центрі цієї трансформації знаходяться Composables: функції, що інкапсулюють реактивний стан, бізнес-логіку та побічні ефекти в незалежні модулі багаторазового використання. На відміну від міксинів Vue 2, які спричиняли конфлікти імен та приховані залежності, Composables забезпечують повну прозорість, статичну типізацію завдяки TypeScript та явну композицію.
Опанування просунутих патернів Composable стало у 2026 році фундаментальною вимогою для позицій рівня middle та senior в екосистемі Vue. У цьому посібнику проаналізовано патерни, які найчастіше зустрічаються на технічних співбесідах — від анатомії добре побудованого Composable, через Dependency Injection з використанням provide/inject, до стратегій тестування кожного реактивного елементу.
Composable — це функція, яка використовує Composition API Vue для інкапсуляції та повторного використання логіки, що містить стан. На відміну від міксинів, Composables пропонують явні входи та виходи, повну інференцію типів TypeScript і повну відсутність колізій імен. Конвенція передбачає іменування Composable з префіксом use (наприклад, useCounter, useFetch).
Анатомія добре побудованого Composable
Продакшн-готовий Composable дотримується передбачуваної структури: приймає конфігурацію через аргументи, створює реактивний стан всередині та надає типізований об'єкт повернення. Цей патерн гарантує композиційність, тестовність та чіткі межі API.
import { ref, computed, type Ref } from 'vue'
interface UseCounterOptions {
min?: number
max?: number
initialValue?: number
}
interface UseCounterReturn {
count: Ref<number>
doubled: Ref<number>
increment: () => void
decrement: () => void
reset: () => void
}
export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
const { min = 0, max = Infinity, initialValue = 0 } = options
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
if (count.value < max) count.value++
}
function decrement() {
if (count.value > min) count.value--
}
function reset() {
count.value = initialValue
}
return { count, doubled, increment, decrement, reset }
}Кілька аспектів цього патерну заслуговують на окрему увагу. Інтерфейс UseCounterReturn явно визначає контракт Composable, що полегшує автодоповнення в редакторах коду та запобігає випадковим змінам у публічному API. Об'єкт опцій зі значеннями за замовчуванням дозволяє гнучко налаштовувати поведінку без перевантаження сигнатури функції. Кожен виклик useCounter() створює незалежний екземпляр стану, що повністю усуває проблеми зі спільним станом, характерні для міксинів.
Явний тип повернення UseCounterReturn виконує подвійну роль: документує публічне API Composable та унеможливлює випадкове розкриття внутрішніх деталей реалізації. Компоненти-споживачі деструктуризують саме те, що їм потрібно, зберігаючи чистоту зв'язків у шаблоні.
Асинхронні Composables з обробкою помилок та станами завантаження
Отримання даних є одним з найпоширеніших сценаріїв використання Composables. Надійний асинхронний Composable керує станом завантаження, обробкою помилок та автоматичним очищенням ресурсів. Ці патерни регулярно перевіряються під час технічних співбесід.
import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<string | null>
isLoading: Ref<boolean>
refresh: () => Promise<void>
}
export function useFetchData<T>(
url: MaybeRefOrGetter<string>
): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const isLoading = ref(false)
let abortController: AbortController | null = null
async function fetchData() {
// Cancel any in-flight request
abortController?.abort()
abortController = new AbortController()
isLoading.value = true
error.value = null
try {
const response = await fetch(toValue(url), {
signal: abortController.signal
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
error.value = err instanceof Error ? err.message : 'Unknown error'
} finally {
isLoading.value = false
}
}
// Re-fetch when URL changes reactively
watchEffect(() => {
fetchData()
})
onUnmounted(() => abortController?.abort())
return { data, error, isLoading, refresh: fetchData }
}Цей Composable демонструє кілька просунутих концепцій. Тип параметра MaybeRefOrGetter<string> приймає як звичайні рядки, так і Ref-об'єкти чи функції-геттери, що забезпечує максимальну гнучкість для коду, що викликає. Функція toValue() витягує базове значення незалежно від типу вхідних даних. Використання AbortController запобігає перегонам (race conditions), скасовуючи застарілі запити в момент зміни URL, перш ніж попередня відповідь встигне надійти.
watchEffect встановлює автоматичне реактивне відстеження: кожен Ref або геттер, до якого здійснюється доступ всередині колбеку, автоматично стає залежністю. Коли URL є Ref-об'єктом і його значення змінюється, ефект автоматично виконується повторно. Хук onUnmounted гарантує очищення ресурсів після знищення компонента, запобігаючи витокам пам'яті.
Composables, що викликають onMounted, onUnmounted або інші хуки життєвого циклу, мають бути викликані синхронно всередині setup(). Виклик Composable всередині асинхронного колбеку або setTimeout призведе до того, що хук життєвого циклу мовчки не зареєструється, оскільки відсутня активна інстанція компонента.
Композиція Composables: абстракції вищого рівня
Справжня сила Composables розкривається, коли вони компонують інші Composables. Це відображає принцип функціональної композиції: малі, сфокусовані на одному завданні модулі об'єднуються в складну поведінку без необхідності використання ієрархій наслідування.
import { ref, computed, watch, type Ref } from 'vue'
import { useFetchData } from './useFetchData'
import { useDebouncedRef } from './useDebouncedRef'
interface UsePaginatedSearchReturn<T> {
query: Ref<string>
page: Ref<number>
results: Ref<T[] | null>
totalPages: Ref<number>
isLoading: Ref<boolean>
error: Ref<string | null>
nextPage: () => void
prevPage: () => void
}
export function usePaginatedSearch<T>(
baseUrl: string,
perPage = 20
): UsePaginatedSearchReturn<T> {
const query = useDebouncedRef('', 300)
const page = ref(1)
const totalPages = ref(1)
const apiUrl = computed(
() => `${baseUrl}?q=${encodeURIComponent(query.value)}&page=${page.value}&limit=${perPage}`
)
const { data, error, isLoading } = useFetchData<{ items: T[]; total: number }>(apiUrl)
const results = computed(() => data.value?.items ?? null)
watch(data, (response) => {
if (response) {
totalPages.value = Math.ceil(response.total / perPage)
}
})
// Reset to page 1 when query changes
watch(query, () => { page.value = 1 })
function nextPage() {
if (page.value < totalPages.value) page.value++
}
function prevPage() {
if (page.value > 1) page.value--
}
return { query, page, results, totalPages, isLoading, error, nextPage, prevPage }
}Композиція відбувається на кількох рівнях. useDebouncedRef надає Ref з вбудованим дебаунсингом, що запобігає надмірним запитам під час введення тексту. useFetchData отримує apiUrl як computed, що означає, що кожна зміна query або page перераховує URL та автоматично ініціює новий запит. Спостерігач за query скидає пагінацію на сторінку 1 при кожній зміні пошукової фрази.
Цей патерн демонструє фундаментальний принцип: кожен Composable вирішує конкретну задачу, а композиція поєднує їх без створення зв'язностей. Під час технічної співбесіди пояснення цього ланцюга реактивності (query змінюється -> apiUrl перераховується -> useFetchData виконує повторний запит -> results оновлюється) свідчить про глибоке розуміння системи реактивності Vue.
Готовий до співбесід з Vue.js / Nuxt.js?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Dependency Injection з provide/inject в архітектурі Composables
Деякі стани мають бути спільними в межах дерева компонентів без необхідності передачі пропсів через кожен рівень ієрархії. Система provide/inject Vue у поєднанні з Composables створює типізований та безпечний патерн контексту.
import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue'
type Theme = 'light' | 'dark' | 'system'
interface ThemeContext {
theme: Readonly<Ref<Theme>>
setTheme: (t: Theme) => void
resolvedTheme: Readonly<Ref<'light' | 'dark'>>
}
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export function provideTheme(initial: Theme = 'system') {
const theme = ref<Theme>(initial)
const resolvedTheme = computed<'light' | 'dark'>(() => {
if (theme.value !== 'system') return theme.value
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
})
function setTheme(t: Theme) {
theme.value = t
}
const context: ThemeContext = {
theme: readonly(theme),
setTheme,
resolvedTheme: readonly(resolvedTheme)
}
provide(ThemeKey, context)
return context
}
export function useTheme(): ThemeContext {
const context = inject(ThemeKey)
if (!context) {
throw new Error('useTheme() requires a parent component to call provideTheme()')
}
return context
}Цей патерн чітко розділяє надання контексту та його споживання. Кореневий компонент (або layout) викликає provideTheme() одноразово, а будь-який дочірній компонент може отримати доступ до контексту через useTheme(), незалежно від глибини вкладеності в дереві компонентів. Використання InjectionKey<ThemeContext> із Symbol гарантує безпеку типів на етапі компіляції.
Функція readonly() обгортає експоновані Ref-об'єкти, щоб запобігти прямим мутаціям з боку компонентів-споживачів. Лише setTheme() може модифікувати тему, забезпечуючи однонаправлений потік даних. Властивість resolvedTheme розв'язує значення 'system' шляхом запиту до media query браузера, надаючи завжди конкретне значення ('light' або 'dark'), яке компоненти можуть безпосередньо використати для застосування стилів.
На технічних співбесідах цей патерн часто порівнюють із React Context або сторами Pinia. Ключова відмінність полягає в тому, що provide/inject працює на рівні дерева компонентів (а не глобально) і не потребує зовнішніх залежностей. Це забезпечує точний контроль над областю видимості спільного стану.
Composable для валідації форм
Валідація форм — це сфера, де Composables проявляють себе особливо ефективно. Вони замінюють повторювану логіку декларативним, заснованим на правилах патерном, який можна повторно використовувати у всьому застосунку.
import { reactive, computed, type UnwrapNestedRefs } from 'vue'
type ValidationRule<T> = (value: T) => string | true
type FieldRules<T> = { [K in keyof T]?: ValidationRule<T[K]>[] }
interface UseFormReturn<T extends Record<string, any>> {
fields: UnwrapNestedRefs<T>
errors: Record<keyof T, string>
isValid: Ref<boolean>
validate: () => boolean
resetErrors: () => void
}
export function useFormValidation<T extends Record<string, any>>(
initialValues: T,
rules: FieldRules<T>
): UseFormReturn<T> {
const fields = reactive({ ...initialValues }) as UnwrapNestedRefs<T>
const errors = reactive(
Object.keys(initialValues).reduce(
(acc, key) => ({ ...acc, [key]: '' }),
{} as Record<keyof T, string>
)
)
function validate(): boolean {
let valid = true
for (const key of Object.keys(rules) as (keyof T)[]) {
const fieldRules = rules[key] || []
errors[key] = '' as any
for (const rule of fieldRules) {
const result = rule(fields[key])
if (result !== true) {
errors[key] = result as any
valid = false
break // Stop at first error per field
}
}
}
return valid
}
function resetErrors() {
for (const key of Object.keys(errors)) {
(errors as any)[key] = ''
}
}
const isValid = computed(() =>
Object.values(errors).every((e) => e === '')
)
return { fields, errors, isValid, validate, resetErrors }
}Composable приймає початкові значення та мапу правил валідації для кожного поля. Кожне правило — це функція, що повертає true у разі коректного значення або рядок з повідомленням про помилку. Метод validate() ітерує правилами та зупиняється на першій помилці кожного поля, щоб уникнути одночасного відображення кількох повідомлень, що може дезорієнтувати користувачів.
Використання reactive() замість ref() для полів та помилок спрощує доступ у шаблонах: fields.email замість fields.value.email. Обчислювана властивість isValid автоматично оновлюється при кожній зміні помилки, дозволяючи реактивно вмикати або вимикати кнопки відправлення форми.
Цей патерн легко розширюється. Асинхронні правила (перевірка доступності електронної адреси), крос-полева валідація (підтвердження пароля) та інтернаціоналізовані повідомлення про помилки можуть бути додані без модифікації базової структури Composable. У продакшн-середовищі цей патерн часто поєднують із бібліотекою на кшталт Zod для визначення схем валідації.
Тестування Composables в ізоляції
Composables, що використовують реактивне API Vue, потребують контексту компонента для коректної роботи. Допоміжна функція withSetup вирішує цю вимогу, створюючи мінімальний компонент, який виконує Composable всередині setup().
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useCounter } from './useCounter'
function withSetup<T>(composable: () => T): { result: T; unmount: () => void } {
let result!: T
const wrapper = mount(
defineComponent({
setup() {
result = composable()
return () => h('div')
}
})
)
return { result, unmount: () => wrapper.unmount() }
}
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = withSetup(() => useCounter())
expect(result.count.value).toBe(0)
})
it('respects min and max boundaries', () => {
const { result } = withSetup(() =>
useCounter({ min: 0, max: 3, initialValue: 3 })
)
result.increment()
expect(result.count.value).toBe(3) // Capped at max
result.count.value = 0
result.decrement()
expect(result.count.value).toBe(0) // Capped at min
})
it('computes doubled value reactively', () => {
const { result } = withSetup(() => useCounter({ initialValue: 5 }))
expect(result.doubled.value).toBe(10)
result.increment()
expect(result.doubled.value).toBe(12)
})
})Функція withSetup є стандартним патерном в екосистемі Vue для тестування Composables в ізоляції. Вона монтує компонент-обгортку, який викликає Composable всередині setup(), і надає доступ до результату та функції unmount() для симуляції знищення компонента.
Кожен тест валідує конкретний аспект: ініціалізацію зі значеннями за замовчуванням, дотримання налаштованих меж та реактивність обчислюваних значень. Третій тест є особливо важливим, оскільки перевіряє, що doubled автоматично оновлюється після зміни count через increment(), підтверджуючи коректну роботу графу реактивності.
Для асинхронних Composables, таких як useFetchData, тести потребують мокування fetch та обробки промісів за допомогою flushPromises() з Vue Test Utils. Під час технічних співбесід знання цього патерну тестування свідчить про професійну зрілість та увагу до якості коду.
VueUse містить понад 200 продакшн-готових Composables, що охоплюють API браузера, сенсори, анімації та допоміжні функції. Аналіз вихідного коду виявляє послідовні патерни: об'єкти опцій для конфігурації, захисти SSR та tryOnScopeDispose для очищення ресурсів. Бібліотека є чудовим джерелом для прийняття архітектурних рішень щодо Composables.
Типові питання для співбесіди щодо Vue 3 Composables
Наведені нижче питання регулярно зустрічаються у процесах технічних співбесід на позиції рівня middle та senior у сфері Vue у 2026 році:
1. Чим Composable відрізняється від міксину?
Міксини неявно впроваджують властивості в компонент, що призводить до конфліктів імен та ускладнює відстеження походження кожної властивості. Composables повертають значення явно, пропонують повну типізацію TypeScript та дозволяють перейменування змінних під час деструктуризації об'єкта повернення. Кожен виклик створює незалежний екземпляр стану, тоді як міксини поділяють стан між усіма компонентами, які їх включають.
2. Чому Composables мають викликатися всередині setup()?
Vue асоціює хуки життєвого циклу (onMounted, onUnmounted, watch, watchEffect) з активною інстанцією компонента під час виконання setup(). Виклик Composable поза цим контекстом розриває ці асоціації, внаслідок чого ефекти не будуть зареєстровані та очищені. Це одне з найпоширеніших питань на технічних співбесідах, пов'язаних із Vue.
3. Як поділяти стан між кількома компонентами за допомогою Composables?
Існують дві основні стратегії. Для глобального стану Ref оголошується за межами функції Composable (патерн Singleton). Для контекстного стану в піддереві компонентів застосовується provide/inject з типізованим InjectionKey, як продемонстровано у прикладі useTheme. Для складнішого глобального керування станом рекомендовано використання Pinia.
4. Що таке MaybeRefOrGetter і чому це важливо?
Це утилітарний тип Vue, який приймає звичайне значення, Ref або функцію-геттер. Він дозволяє Composables приймати як статичні, так і реактивні вхідні дані, максимізуючи гнучкість для коду, що викликає. Функція toValue() витягує базове значення незалежно від типу вхідних даних.
5. Як тестувати Composables, що використовують onMounted або onUnmounted?
Використовується допоміжна функція типу withSetup, яка монтує компонент-обгортку. Цей компонент виконує Composable всередині setup(), забезпечуючи необхідний контекст інстанції. Для верифікації очищення ресурсів викликається unmount() і перевіряється, чи побічні ефекти (слухачі подій, таймери, AbortController) були видалені.
6. Коли варто використовувати Pinia замість Composables з provide/inject?
Pinia є кращим вибором, коли стан має бути глобально доступним з будь-якого компонента без ієрархічного зв'язку, коли потрібна персистентність (localStorage/sessionStorage), коли необхідна інтеграція з Vue DevTools для налагодження або коли кілька дерев компонентів мають поділяти один і той самий стан. Composables з provide/inject краще підходять для контекстного стану, обмеженого конкретним піддеревом компонентів.
Висновок
Vue 3 Composables — це значно більше, ніж альтернатива міксинам: це парадигма організації коду, яка масштабується від простих допоміжних функцій до складних систем з Dependency Injection та багаторівневою композицією. Опанування цих патернів відрізняє розробників, які просто використовують Vue, від тих, хто розуміє систему реактивності на глибинному рівні.
Ключові висновки:
- Чітка структура: Типізовані інтерфейси входу та виходу (
UseXxxOptionsтаUseXxxReturn) документують API та запобігають випадковому розкриттю внутрішніх деталей реалізації - Ізоляція стану: Кожен виклик створює незалежний екземпляр, повністю усуваючи конфлікти
- Явна композиція: Composables, що споживають інші Composables, створюють передбачувані ланцюги реактивності
- Керування ресурсами:
AbortControllerтаonUnmountedгарантують очищення побічних ефектів і запобігають витокам пам'яті - Типізована ін'єкція:
provide/injectзInjectionKeyзамінює prop drilling без втрати безпеки типів - Тестовність: Патерн
withSetupдозволяє валідувати реактивність у повній ізоляції - Прагматизм: Перевірка VueUse перед реімплементацією типових функціональностей економить час та використовує перевірені рішення
Підготовка цих патернів з конкретними реалізаціями та здатністю пояснити проєктні рішення, що стоять за кожним із них, забезпечує значну перевагу у технічних процесах відбору на позиції Vue у 2026 році.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Nuxt 4 у 2026 році: Нова структура каталогів та міграція з Nuxt 3
Повний посібник з міграції на Nuxt 4: нова структура app/, singleton-шар отримання даних, поверхнева реактивність за замовчуванням, розділення TypeScript-контексту, нормалізовані імена компонентів, Vue Router v5, зміни в управлінні head та контрольний список міграції.

Vue 3 Pinia vs Vuex: Сучасне управління станом та питання для співбесід 2026
Порівняння Pinia та Vuex: архітектура, TypeScript, Composition API, міграція, гідратація SSR та найпоширеніші питання зі співбесід щодо управління станом Vue у 2026 році.

Ключові запитання співбесід із Vue.js: 25 запитань, щоб отримати роботу
Підготуйтеся до співбесід із Vue.js, маючи в арсеналі 25 ключових запитань. Від реактивності до composables — опануйте найважливіше для наступної зустрічі.