Vue 3 Composition API я держу под рукой на каждом дашборде, который собираю за последний год - от панели с метриками эквайринга T‑Bank до трекинга отправлений СДЭК в личном кабинете магазина. Когда на экране 15-20 виджетов, у каждого своё состояние, свои фильтры и своё обновление по таймеру, Options API расползается: логика одного графика оказывается размазана по data, computed, methods и watch, а рядом лежит логика ещё пяти. Композиционный API собирает всё это в одну функцию, которую видно целиком и которую я переношу в соседний проект копипастом. Ниже - как это выглядит на боевых панелях, с кодом, цифрами и граблями, на которые я наступал.
Почему композиционный API удобнее Options API на дашбордах
Дашборд отличается от обычной страницы тем, что виджеты живут своей жизнью: один тянет продажи WooCommerce за неделю, второй крутит статус доставок, третий раз в минуту дёргает вебхук из n8n. В Options API каждый такой блок вынуждает лезть в четыре разных секции компонента. В Composition API одна фича - одна функция, и переиспользование получается почти бесплатным.
Вот честное сравнение по тем критериям, которые реально болят на больших панелях:
| Критерий | Options API | Composition API |
|---|---|---|
| Логика одного виджета | Размазана по data / computed / methods / watch | Собрана в одной функции-композибле |
| Переиспользование между панелями | Миксины с конфликтами имён | Импорт композибла, ноль магии |
| Читаемость при 15+ виджетах | Файл на 600 строк, скролл вслепую | setup из 5-6 понятных вызовов |
| TypeScript | Работает, но с бубном | Типы выводятся из ref и computed сами |
| Тестирование логики | Только через монтирование компонента | Композибл тестируется как обычная функция |
На панели для интернет-магазина, где было 18 виджетов, переход с миксинов на композиблы сократил главный компонент дашборда с 640 строк до 90 - вся тяжёлая логика уехала в шесть отдельных файлов в папке composables, и каждый я потом переиспользовал в админке на другом проекте без единой правки.
Реактивное состояние: ref, reactive и computed
Первое, обо что спотыкаются те, кто пришёл с Options API - ref против reactive. Моё правило после десятка панелей простое: примитивы и одиночные значения (число, строка, флаг загрузки) - через ref; связный объект состояния фильтров - через reactive, но без фанатизма. ref требует .value в скрипте, зато его нельзя случайно «разложить» деструктуризацией и потерять реактивность, а с reactive это происходит регулярно.
Производные метрики - только computed, никогда не пересчёт вручную во watch. Сумма по строкам таблицы, средний чек, конверсия - всё это чистые вычисления от исходных данных, и computed кэширует результат, пока источник не изменился. На дашборде с 30-секундным поллингом это экономит десятки лишних перерасчётов в минуту.
import { ref, computed, onMounted, onUnmounted } from 'vue'
export function useDashboardData(endpoint, intervalMs = 30000) {
const rows = ref([])
const loading = ref(false)
const error = ref(null)
let timer = null
async function load() {
loading.value = true
try {
const res = await fetch(endpoint)
rows.value = await res.json()
error.value = null
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
const total = computed(() =>
rows.value.reduce((sum, r) => sum + r.amount, 0)
)
onMounted(() => {
load()
timer = setInterval(load, intervalMs)
})
onUnmounted(() => clearInterval(timer))
return { rows, loading, error, total, reload: load }
}
Эта функция - самодостаточный кусок дашборда: она сама грузит данные, сама обновляет их по таймеру и сама подчищает таймер при размонтировании. В компоненте её подключение занимает одну строку.
<script setup>
import { useDashboardData } from '@/composables/useDashboardData'
const { rows, total, loading, reload } = useDashboardData('/api/orders')
</script>
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Композиблы: выносим логику виджетов из компонента
Композибл - обычная функция, которая начинается с use и возвращает реактивное состояние вместе с методами. Смысл в том, что setup перестаёт быть свалкой и превращается в оглавление: видно, из чего собрана панель, ещё до того как ты открыл сами файлы.
Я раскладываю композиблы по зонам ответственности:
- useDashboardData - загрузка и поллинг одного источника, как выше;
- useFilters - реактивный объект фильтров (диапазон дат, статус заказа, город доставки) и его сериализация в query-строку;
- useChart - жизненный цикл инстанса графика, о нём отдельно ниже;
- useExport - выгрузка текущей выборки в CSV, которую менеджеры просят в 9 случаях из 10.
Отдельная выгода - тестируемость. Композибл useFilters я покрываю юнит-тестами напрямую, без монтирования компонента: вызвал функцию, подёргал возвращённые ref, проверил, что query собралась правильно. На панели заказов это ловило баги с датами до того, как их видел заказчик.
Если пишете композибл, который обращается к жизненному циклу через onMounted или onUnmounted, помните: вызывать его можно только синхронно внутри setup. Спрятать вызов useDashboardData внутрь await или колбэка - и хуки жизненного цикла молча не подключатся, таймер не запустится, а вы будете час гадать, почему виджет не обновляется. Проверено на себе.
Производительность: shallowRef, markRaw и графики
Самая частая просадка на дашбордах - не сеть, а реактивность поверх тяжёлых объектов. Инстанс Chart.js или ApexCharts - это здоровенный объект с сотнями внутренних полей. Если положить его в обычный ref, Vue рекурсивно сделает реактивным каждое из этих полей, и при обновлении данных браузер начинает подтормаживать на ровном месте.
Лечится двумя приёмами. Сам инстанс графика оборачиваю в markRaw, чтобы Vue вообще не трогал его прокси, и держу ссылку в shallowRef - реактивной остаётся только сама ссылка, а не внутренности.
import { shallowRef, markRaw, watch } from 'vue'
import { Chart } from 'chart.js/auto'
const chart = shallowRef(null)
function mountChart(canvas, data) {
chart.value = markRaw(new Chart(canvas, {
type: 'line',
data
}))
}
watch(() => props.series, (next) => {
if (!chart.value) return
chart.value.data.datasets[0].data = next
chart.value.update()
})
Второе - не перерисовывать график целиком при каждом обновлении. Через watch я меняю данные внутри существующего инстанса и вызываю его update(), а не пересоздаю компонент. На панели с восемью графиками, которые обновлялись раз в 15 секунд, связка shallowRef + markRaw + точечный update() убрала фризы интерфейса, которые до этого длились по 200-400 мс на каждом тике.
Отдельно про большие таблицы: если строк больше пары тысяч, реактивность самого массива уже не главная беда - тормозит DOM. Тут спасает виртуализация списка, а не борьба с ref. Vue 3 сам по себе держит десятки тысяч реактивных полей спокойно, проблема почти всегда в рендере, а не в реактивной системе.
Когда панель нужна быстро, с интеграциями к эквайрингу и доставке и без месяца на архитектуру, я беру разработку дашборда на Vue.js под ключ от 90 000 ₽ - с уже готовым набором композиблов под поллинг, фильтры и графики, которые не надо изобретать заново.
Живые данные: поллинг, WebSocket и интеграции
Дашборд без свежих данных - картинка. Способ обновления выбираю по задаче, а не по моде.
- Поллинг раз в 15-60 секунд закрывает 80% случаев: метрики продаж, статусы заказов, остатки. Реализуется тем самым
setIntervalвнутри композибла, дёшево и предсказуемо. - WebSocket беру, когда данные должны прилетать в реальном времени - например, новые заявки, которые параллельно улетают в Telegram через aiogram-бота. Подключение оборачиваю в
useSocket, а входящие сообщения кладу в тот жеref, что и поллинг, чтобы виджет не знал, откуда пришло обновление. - Вебхуки через n8n - удобный слой между внешними сервисами и дашбордом. СДЭК дёргает n8n на смену статуса отправления, n8n складывает событие в базу, а панель забирает его поллингом. Не нужно, чтобы фронтенд напрямую держал десяток интеграций.
Ещё один сценарий из практики - встроить готовый Vue-виджет с метриками прямо в лендинг на Tilda через блок с кодом. Composition API тут выигрывает, потому что виджет монтируется точечно в конкретный div, тянет свой композибл с данными и не тащит за собой оформление всей страницы.
Важный момент про отписки: любой источник живых данных нужно гасить в onUnmounted - закрыть сокет, снять интервал. Забытый таймер на панели, которую пользователь открывает и закрывает по десять раз за смену, за час превращается в горсть висящих запросов и утёкшую память. Композибл хорош тем, что очистку я пишу рядом с подпиской, в той же функции, и забыть про неё физически сложнее.
Интерфейс аналитики и отчётов
Дашборд / BI
от 90 000 ₽
Подробнее →Частые вопросы
Стоит ли переписывать существующий дашборд с Options API на Composition API?
Если панель работает и её почти не трогают - нет, переписывание ради переписывания только добавит багов. А вот если дашборд активно растёт, каждый новый виджет причиняет боль и файл компонента распух за 500 строк, миграцию окупает уже второй-третий новый виджет. Vue 3 поддерживает оба API одновременно, поэтому переводить можно постепенно: новые фичи писать на композиблах, старые не трогать, пока руки не дойдут.
Что выбрать для состояния - ref или reactive?
Я по умолчанию беру ref для всего: примитивов, массивов, объектов. Он предсказуемее, его нельзя случайно потерять при деструктуризации, и он единообразно выглядит в коде. reactive оставляю для случаев, когда действительно удобно работать со связным объектом без .value на каждом поле - например, объект фильтров, который целиком уходит в форму. Мешать оба стиля в одном композибле без причины не стоит.
Как Composition API дружит с TypeScript на дашборде?
Очень хорошо, и это одна из причин, по которой я на нём и сижу. Типы выводятся из ref и computed автоматически, дженерики у ref<Order[]>() работают понятно, а композиблы отдают наружу уже типизированное состояние. В связке с <script setup> типизация пропсов и эмитов получается лаконичной - без громоздких деклараций, которые требовал Options API.
Тормозит ли реактивность Vue 3 при большом объёме данных?
Сама реактивная система тянет десятки тысяч полей спокойно. Тормоза почти всегда в двух местах: тяжёлые внешние объекты вроде инстансов графиков, положенные в глубокий ref вместо shallowRef с markRaw, и рендер огромных таблиц без виртуализации. Уберите эти две причины - и панель с живым обновлением по таймеру работает плавно даже на слабых ноутбуках у менеджеров.