Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Composition API
vue_composition_api

Composition API

ref, reactive, computed, watch, watchEffect, lifecycle hooks и синтаксический сахар script setup

Composition API

Composition API — это альтернативный способ организации логики компонента, введённый в Vue 3. Вместо разделения по «опциям» (data, methods, computed) логика группируется по смыслу в виде функций.

#Зачем Composition API: проблема Options API при росте компонентов

Представьте компонент управления пользователями: поиск, сортировка, пагинация, модальные окна, загрузка данных. В Options API всё это перемешивается: логика поиска размазана между data, computed и methods. Чтобы понять, как работает одна фича, нужно прыгать между разными секциями файла.

Options API отлично работает для простых компонентов. Но когда компонент превышает 200-300 строк, его становится трудно читать и ещё труднее переиспользовать логику между компонентами. Исторически единственным решением были миксины — но они создавали конфликты имён и скрытые зависимости.

Composition API решает это: вы группируете связанную логику в функции, называемые composables, и переиспользуете их между компонентами без этих проблем.

#setup() функция — точка входа

Функция setup() — это точка входа для Composition API. Она вызывается до создания компонента, до created хука. В ней объявляются все реактивные данные, вычисляемые значения, методы — и всё это возвращается для использования в шаблоне:

<template> <div> <p>{{ count }}</p> <button @click="increment">+1</button> </div> </template> <script> import { ref } from 'vue' export default { setup() { // Всё что вернём — доступно в шаблоне const count = ref(0) function increment() { count.value++ } // Обязательно нужно вернуть return { count, increment } } } </script>

Обратите внимание: в setup() нет доступа к this. Это намеренное решение — композируемые функции должны быть чистыми и не зависеть от контекста компонента.

#ref() — реактивные примитивы

ref() создаёт реактивную ссылку на значение. Оборачивает значение в объект с полем .value. Это необходимо для отслеживания изменений примитивов (строк, чисел, булевых значений):

<script setup> import { ref } from 'vue' const count = ref(0) // число const name = ref('Иван') // строка const isLoading = ref(false) // булево const items = ref([]) // массив тоже можно через ref console.log(count.value) // 0 — доступ через .value в JS count.value++ // изменение через .value // В шаблоне Vue автоматически разворачивает .value: // {{ count }} — не нужно писать {{ count.value }} </script> <template> <p>{{ count }}</p> <!-- Vue сам подставит count.value --> </template>

ref работает с любым типом данных, включая объекты и массивы. Для объектов под капотом используется reactive, поэтому вложенные свойства тоже реактивны.

#reactive() — реактивные объекты

reactive() создаёт глубоко реактивный объект. Не нужен .value — доступ к свойствам прямой:

<script setup> import { reactive } from 'vue' const user = reactive({ name: 'Иван', email: 'ivan@example.com', address: { city: 'Москва', street: 'Арбат' } }) // Прямой доступ — без .value user.name = 'Пётр' user.address.city = 'Питер' // Глубокая реактивность работает! // В шаблоне: // {{ user.name }}, {{ user.address.city }} </script>

Ограничение reactive: нельзя заменить объект целиком — потеряется реактивность. Правильный способ — обновлять свойства:

<script setup> import { reactive } from 'vue' const state = reactive({ count: 0, user: null }) // НЕПРАВИЛЬНО — потеря реактивности: // state = reactive({ count: 10, user: { name: 'Иван' } }) // ПРАВИЛЬНО — обновляем свойства: state.count = 10 state.user = { name: 'Иван' } </script>

#ref vs reactive: когда что использовать

Оба инструмента создают реактивные данные, но у каждого свои сценарии применения.

Используйте ref для примитивов (числа, строки, булевы значения) и когда нужно переприсваивать значение целиком:

<script setup> import { ref, reactive } from 'vue' // ref — примитивы и значения, которые меняются целиком const count = ref(0) const selectedUser = ref(null) const tags = ref(['vue', 'js']) // Переприсвоение работает корректно: selectedUser.value = { id: 1, name: 'Иван' } // OK tags.value = ['vue', 'ts'] // OK // reactive — объекты с несколькими полями, стейт формы const form = reactive({ username: '', password: '', rememberMe: false }) // Удобнее обновлять несколько полей: Object.assign(form, { username: 'ivan', rememberMe: true }) </script>

На практике большинство Vue-разработчиков предпочитают ref везде — его поведение предсказуемее. reactive удобен для форм и сложных стейт-объектов.

#computed() — вычисляемые значения

computed() создаёт вычисляемое реактивное значение, которое кэшируется и пересчитывается только при изменении зависимостей:

<script setup> import { ref, computed } from 'vue' const firstName = ref('Иван') const lastName = ref('Петров') const items = ref([ { name: 'Яблоко', category: 'fruits', price: 50 }, { name: 'Банан', category: 'fruits', price: 30 }, { name: 'Молоко', category: 'dairy', price: 80 } ]) const filter = ref('fruits') // Простое вычисление const fullName = computed(() => `${firstName.value} ${lastName.value}`) // Сложная фильтрация — пересчитывается только при изменении items или filter const filteredItems = computed(() => items.value.filter(item => item.category === filter.value) ) // Computed с сеттером const fullNameWithSetter = computed({ get: () => `${firstName.value} ${lastName.value}`, set: (value) => { const [first, last] = value.split(' ') firstName.value = first lastName.value = last || '' } }) </script>

Computed-значение доступно через .value в скрипте и напрямую в шаблоне. Оно только для чтения по умолчанию — для изменения нужен сеттер.

#watch() — наблюдение за изменениями

watch() позволяет реагировать на изменения конкретного реактивного значения. Полезен для side effects: запросы к API, логирование, синхронизация:

<script setup> import { ref, watch } from 'vue' const searchQuery = ref('') const userId = ref(null) const user = ref(null) // Простое наблюдение за ref watch(searchQuery, (newValue, oldValue) => { console.log(`Поиск изменился с "${oldValue}" на "${newValue}"`) performSearch(newValue) }) // immediate: true — запустить немедленно при инициализации watch(userId, async (newId) => { if (newId) { user.value = await fetchUser(newId) } }, { immediate: true }) // deep: true — глубокое наблюдение за объектом const userProfile = ref({ name: 'Иван', settings: { theme: 'dark' } }) watch(userProfile, (newValue) => { saveToLocalStorage(newValue) }, { deep: true }) // Наблюдение за несколькими источниками watch([searchQuery, userId], ([newQuery, newId]) => { console.log('Изменилось что-то из этих:', newQuery, newId) }) // Наблюдение за геттером (вычисляемым выражением) watch( () => userProfile.value.settings.theme, (newTheme) => { document.body.dataset.theme = newTheme } ) async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) return response.json() } async function performSearch(query) { // ... } function saveToLocalStorage(data) { localStorage.setItem('profile', JSON.stringify(data)) } </script>

#watchEffect() — автоматическое отслеживание зависимостей

watchEffect() запускает функцию немедленно и автоматически перезапускает её при изменении любого реактивного значения, использованного внутри. Не нужно явно указывать зависимости:

<script setup> import { ref, watchEffect } from 'vue' const count = ref(0) const name = ref('Иван') // Автоматически отслеживает все зависимости — count и name watchEffect(() => { console.log(`count: ${count.value}, name: ${name.value}`) // Этот код запустится сразу и потом при каждом изменении count или name }) // Очистка ресурсов внутри watchEffect watchEffect((onCleanup) => { const timer = setInterval(() => { console.log('тик', count.value) }, 1000) // onCleanup вызывается перед следующим запуском или при размонтировании onCleanup(() => clearInterval(timer)) }) </script>

Разница между watch и watchEffect:

  • watch — явно указываете что наблюдать, получаете старое и новое значение, ленивый (не запускается сразу по умолчанию)
  • watchEffect — автоматически отслеживает зависимости, запускается сразу, нет доступа к старому значению

#Lifecycle hooks в Composition API

В Composition API хуки жизненного цикла импортируются из Vue и вызываются внутри setup():

<script setup> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' const data = ref(null) let pollInterval = null onMounted(async () => { // DOM доступен — подходит для работы с DOM, загрузки данных data.value = await fetchInitialData() pollInterval = setInterval(pollData, 5000) }) onUpdated(() => { // Вызывается после обновления DOM // Осторожно: не изменяйте данные здесь — это вызовет бесконечный цикл }) onUnmounted(() => { // Очищаем ресурсы чтобы избежать утечек памяти clearInterval(pollInterval) }) async function fetchInitialData() { const res = await fetch('/api/data') return res.json() } async function pollData() { data.value = await fetchInitialData() } </script>

Важный момент: в Composition API нет хуков beforeCreate и created — функция setup() сама выполняется на этом этапе.

#script setup — синтаксический сахар

<script setup> — самый удобный способ использовать Composition API. Он устраняет необходимость явно возвращать значения из setup(). Всё что объявлено в <script setup> автоматически доступно в шаблоне:

<template> <!-- count, doubled, increment — всё доступно напрямую --> <div> <p>{{ count }} (удвоено: {{ doubled }})</p> <button @click="increment">+1</button> <UserCard :user="currentUser" /> </div> </template> <script setup> import { ref, computed } from 'vue' import UserCard from './UserCard.vue' // Компоненты тоже не нужно регистрировать! // Это всё автоматически доступно в шаблоне: const count = ref(0) const doubled = computed(() => count.value * 2) function increment() { count.value++ } const currentUser = ref({ name: 'Иван', role: 'admin' }) </script>

Также в <script setup> доступны специальные макросы компилятора: defineProps, defineEmits, defineExpose. Они не нужно импортировать:

<script setup> // Объявление props const props = defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 } }) // Объявление событий const emit = defineEmits(['update', 'close']) function handleUpdate() { emit('update', { newCount: props.count + 1 }) } // Что экспортировать для родительского компонента через ref defineExpose({ handleUpdate }) </script>

#Composables — переиспользование логики

Главная суперсила Composition API — composables: функции с префиксом use, которые инкапсулируют реактивную логику и могут использоваться в любом компоненте:

<!-- useCounter.js — переиспользуемый composable -->

Создадим несколько composables как отдельные файлы:

// composables/useCounter.js import { ref, computed } from 'vue' export function useCounter(initialValue = 0) { const count = ref(initialValue) const doubled = computed(() => count.value * 2) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = initialValue } return { count, doubled, increment, decrement, reset } }
// composables/useFetch.js import { ref, watch } from 'vue' export function useFetch(url) { const data = ref(null) const error = ref(null) const isLoading = ref(false) async function fetchData() { isLoading.value = true error.value = null try { const response = await fetch(url.value ?? url) if (!response.ok) throw new Error(`HTTP ${response.status}`) data.value = await response.json() } catch (e) { error.value = e.message } finally { isLoading.value = false } } // Если url реактивный — перезапрашиваем при изменении if (typeof url !== 'string') { watch(url, fetchData, { immediate: true }) } else { fetchData() } return { data, error, isLoading, refetch: fetchData } }
// composables/useLocalStorage.js import { ref, watch } from 'vue' export function useLocalStorage(key, defaultValue) { const stored = localStorage.getItem(key) const value = ref(stored ? JSON.parse(stored) : defaultValue) watch(value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)) }, { deep: true }) return value }

Теперь используем composables в компонентах:

<template> <div> <p>Счётчик: {{ count }} (удвоено: {{ doubled }})</p> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="reset">Сбросить</button> <div v-if="isLoading">Загрузка...</div> <div v-else-if="error">Ошибка: {{ error }}</div> <ul v-else> <li v-for="post in data" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script setup> import { useCounter } from '@/composables/useCounter' import { useFetch } from '@/composables/useFetch' // Используем два composable одновременно — никаких конфликтов имён const { count, doubled, increment, decrement, reset } = useCounter(10) const { data, error, isLoading } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5') </script>

Composables — это мощный паттерн. В следующих темах мы увидим их в действии при работе с Vue Router, Pinia и Nuxt.

Далее: Продвинутые компоненты