Vue
Composables: переиспользуемая логика
В дашборде аналитики десяток компонентов независимо тянут данные с сервера: каждый держит свои ref для loading, error и data, дублирует try/catch и сброс ошибки перед запросом. Когда продукт-менеджер просит добавить повтор при сбое сети, правку приходится вносить в десяти местах. Composable собирает это состояние с поведением в одну функцию useFetch, и теперь логика живёт в одном файле, а компоненты лишь вызывают её. Это тот же приём, что хуки в React, но без правил вызова в строгом порядке.
- useFetch: загрузка данных с loading/error/data в одной функции вместо копирования в каждом компоненте
- useLocalStorage: реактивное значение, синхронизированное с localStorage, переживающее перезагрузку страницы
- useMouse, useWindowSize, useDark: готовые composable из VueUse, более 200 функций без написания своих
- useForm: валидация, состояние полей и отправка формы, переиспользуемые между формой входа, регистрации и оплаты
Предварительные знания
- ref, reactive и computed для создания реактивного состояния
- Понимание, что функция может возвращать объект с несколькими значениями
- Знакомство с хуками жизненного цикла на уровне onMounted
Что такое composable
Composable - это обычная функция, которая использует Composition API (ref, computed, хуки) и возвращает реактивное состояние с методами для управления им. Цель - вынести логику с состоянием из компонента, чтобы переиспользовать её, не дублируя код. По соглашению имя начинается с use, что сразу сигнализирует: внутри есть реактивное состояние и, возможно, побочные эффекты.
Каждый вызов composable создаёт свой независимый экземпляр состояния: два компонента, вызвавшие useCounter, получают разные count. Это отличает composable от глобального стора, где состояние общее для всех.
Что отличает composable от обычной вспомогательной функции (утилиты вроде форматирования даты)?
Почему возвращают ref, а не значения
Composable возвращает именно ref-ы (или computed), а не их .value. Причина в деструктуризации: при разборе объекта обычное значение копируется и теряет связь с источником, а ref остаётся ссылкой на реактивную ячейку. Поэтому компонент может деструктурировать результат и сохранить реактивность - изменение внутри composable отразится в шаблоне.
- return { width } (ref) — Деструктуризация в компоненте сохраняет реактивность: const { width } = useWidth() остаётся живым
- return { width: width.value } (значение) — Деструктуризация даёт снимок: число копируется один раз и больше не обновляется
Если composable принимает аргумент, который может быть и обычным значением, и ref, его оборачивают в toRef или toValue. Это позволяет вызывать функцию и как useTitle('Главная'), и как useTitle(someRef), не разбирая случаи вручную.
Почему composable возвращает ref, а не результат обращения к .value?
Самоочистка через хуки жизненного цикла
Сила composable в том, что подписка и её очистка живут в одной функции. Когда composable вызывается в setup компонента, хуки onMounted и onUnmounted внутри него привязываются к жизненному циклу этого компонента. Компонент просто вызывает useXxx и не думает об отписке - composable снимет слушатель сам при размонтировании.
Composable, регистрирующий хуки жизненного цикла, должен вызываться синхронно в теле setup. Вызов внутри обработчика события или после await оставит хуки без активного экземпляра компонента, и onUnmounted не сработает.
Что даёт вызов onMounted и onUnmounted внутри самого composable, а не в компоненте?
VueUse: готовые composable
VueUse - коллекция из более чем 200 готовых composable для типичных задач: useLocalStorage, useDark, useWindowSize, useDebounceFn, useIntersectionObserver и многие другие. Каждый написан с правильной очисткой и поддержкой SSR, поэтому распространённые сценарии не нужно реализовывать заново. Свой composable пишут, когда логика специфична для предметной области проекта.
| Composable | Задача |
|---|---|
| useLocalStorage | Реактивное значение, сохраняемое между перезагрузками |
| useDark | Тёмная тема с учётом системных настроек |
| useWindowSize | Реактивные ширина и высота окна |
| useDebounceFn | Дебаунс функции, например для поля поиска |
| useFetch | Загрузка данных с состоянием loading и error |
Решение писать свой composable или брать из VueUse сводится к одному вопросу: задача общая или специфичная для проекта. Синхронизация с localStorage или отслеживание размера окна - общие, их берут готовыми. Логика расчёта корзины с правилами скидок конкретного магазина - своя.
Когда разумно написать свой composable вместо использования VueUse?
Связь с другими темами
Composable опирается на реактивность и хуки и становится единицей переиспользования:
- Хуки жизненного цикла — Внутри composable вызывают onMounted и onUnmounted, чтобы инкапсулировать подписку и очистку
- ref и reactive — Composable возвращает именно ref-ы, чтобы реактивность сохранялась при деструктуризации
Итог
- Composable - это функция, инкапсулирующая реактивное состояние вместе с управляющей им логикой
- Соглашение по именованию: имя начинается с use (useFetch, useMouse), как и встроенные функции Composition API
- Composable возвращает ref-ы, а не их .value, иначе деструктуризация разорвёт реактивность
- Хуки жизненного цикла внутри composable дают самоочистку: подписка в onMounted, отписка в onUnmounted
- VueUse даёт более 200 готовых composable; свой пишут, когда логика специфична для проекта
Связанные уроки
- vue-11-lifecycle — Composable с подпиской регистрирует onMounted и onUnmounted внутри себя, поэтому опирается на знание хуков
- vue-10-component-communication — И composable, и props/emit - способы разделить ответственность, но composable вытаскивает логику, а не данные между компонентами