# Просунуті Vue 3 Composables: патерни повторного використання та питання для співбесіди 2026 > Вичерпний посібник з просунутих Vue 3 Composables: патерни повторного використання, асинхронна обробка помилок, Dependency Injection, валідація форм та актуальні питання для технічних співбесід у 2026 році. - Published: 2026-05-12 - Updated: 2026-05-12 - Author: SharpSkill - Tags: vue, vue-composables, composition-api, interview - Reading time: 12 min --- 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`, до стратегій тестування кожного реактивного елементу. > **Що таке Vue 3 Composable?** > > Composable — це функція, яка використовує Composition API Vue для інкапсуляції та повторного використання логіки, що містить стан. На відміну від міксинів, Composables пропонують явні входи та виходи, повну інференцію типів TypeScript і повну відсутність колізій імен. Конвенція передбачає іменування Composable з префіксом `use` (наприклад, `useCounter`, `useFetch`). ## Анатомія добре побудованого Composable Продакшн-готовий Composable дотримується передбачуваної структури: приймає конфігурацію через аргументи, створює реактивний стан всередині та надає типізований об'єкт повернення. Цей патерн гарантує композиційність, тестовність та чіткі межі API. ```typescript // useCounter.ts import { ref, computed, type Ref } from 'vue' interface UseCounterOptions { min?: number max?: number initialValue?: number } interface UseCounterReturn { count: Ref doubled: Ref 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 керує станом завантаження, обробкою помилок та автоматичним очищенням ресурсів. Ці патерни регулярно перевіряються під час технічних співбесід. ```typescript // useFetchData.ts import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue' interface UseFetchReturn { data: Ref error: Ref isLoading: Ref refresh: () => Promise } export function useFetchData( url: MaybeRefOrGetter ): UseFetchReturn { const data = ref(null) as Ref const error = ref(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` приймає як звичайні рядки, так і Ref-об'єкти чи функції-геттери, що забезпечує максимальну гнучкість для коду, що викликає. Функція `toValue()` витягує базове значення незалежно від типу вхідних даних. Використання `AbortController` запобігає перегонам (race conditions), скасовуючи застарілі запити в момент зміни URL, перш ніж попередня відповідь встигне надійти. `watchEffect` встановлює автоматичне реактивне відстеження: кожен Ref або геттер, до якого здійснюється доступ всередині колбеку, автоматично стає залежністю. Коли URL є Ref-об'єктом і його значення змінюється, ефект автоматично виконується повторно. Хук `onUnmounted` гарантує очищення ресурсів після знищення компонента, запобігаючи витокам пам'яті. > **Хуки життєвого циклу всередині Composables** > > Composables, що викликають `onMounted`, `onUnmounted` або інші хуки життєвого циклу, мають бути викликані синхронно всередині `setup()`. Виклик Composable всередині асинхронного колбеку або `setTimeout` призведе до того, що хук життєвого циклу мовчки не зареєструється, оскільки відсутня активна інстанція компонента. ## Композиція Composables: абстракції вищого рівня Справжня сила Composables розкривається, коли вони компонують інші Composables. Це відображає принцип функціональної композиції: малі, сфокусовані на одному завданні модулі об'єднуються в складну поведінку без необхідності використання ієрархій наслідування. ```typescript // usePaginatedSearch.ts import { ref, computed, watch, type Ref } from 'vue' import { useFetchData } from './useFetchData' import { useDebouncedRef } from './useDebouncedRef' interface UsePaginatedSearchReturn { query: Ref page: Ref results: Ref totalPages: Ref isLoading: Ref error: Ref nextPage: () => void prevPage: () => void } export function usePaginatedSearch( baseUrl: string, perPage = 20 ): UsePaginatedSearchReturn { 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. ## Dependency Injection з provide/inject в архітектурі Composables Деякі стани мають бути спільними в межах дерева компонентів без необхідності передачі пропсів через кожен рівень ієрархії. Система `provide/inject` Vue у поєднанні з Composables створює типізований та безпечний патерн контексту. ```typescript // useTheme.ts import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue' type Theme = 'light' | 'dark' | 'system' interface ThemeContext { theme: Readonly> setTheme: (t: Theme) => void resolvedTheme: Readonly> } const ThemeKey: InjectionKey = Symbol('theme') export function provideTheme(initial: Theme = 'system') { const theme = ref(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` із `Symbol` гарантує безпеку типів на етапі компіляції. Функція `readonly()` обгортає експоновані Ref-об'єкти, щоб запобігти прямим мутаціям з боку компонентів-споживачів. Лише `setTheme()` може модифікувати тему, забезпечуючи однонаправлений потік даних. Властивість `resolvedTheme` розв'язує значення `'system'` шляхом запиту до media query браузера, надаючи завжди конкретне значення (`'light'` або `'dark'`), яке компоненти можуть безпосередньо використати для застосування стилів. На технічних співбесідах цей патерн часто порівнюють із React Context або сторами Pinia. Ключова відмінність полягає в тому, що `provide/inject` працює на рівні дерева компонентів (а не глобально) і не потребує зовнішніх залежностей. Це забезпечує точний контроль над областю видимості спільного стану. ## Composable для валідації форм Валідація форм — це сфера, де Composables проявляють себе особливо ефективно. Вони замінюють повторювану логіку декларативним, заснованим на правилах патерном, який можна повторно використовувати у всьому застосунку. ```typescript // useFormValidation.ts import { reactive, computed, type UnwrapNestedRefs } from 'vue' type ValidationRule = (value: T) => string | true type FieldRules = { [K in keyof T]?: ValidationRule[] } interface UseFormReturn> { fields: UnwrapNestedRefs errors: Record isValid: Ref validate: () => boolean resetErrors: () => void } export function useFormValidation>( initialValues: T, rules: FieldRules ): UseFormReturn { const fields = reactive({ ...initialValues }) as UnwrapNestedRefs const errors = reactive( Object.keys(initialValues).reduce( (acc, key) => ({ ...acc, [key]: '' }), {} as Record ) ) 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()`. ```typescript // useCounter.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import { defineComponent, h } from 'vue' import { useCounter } from './useCounter' function withSetup(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 як еталонна архітектура** > > 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 році. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns