Composables в Vue.js
Что такое Composables?
Composables — это функции, которые используют Composition API для инкапсуляции и переиспользования состояния и логики между компонентами.
Composables в Vue похожи на custom hooks в React.
Базовый пример
// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}
Использование:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>
Соглашения об именовании
Composables обычно называются с префиксом use:
// Хорошо
useCounter()
useMouse()
useFetch()
useLocalStorage()
// Плохо
counter()
mouse()
fetch()
Примеры популярных Composables
useMouse
Отслеживание позиции мыши:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
Использование:
<template>
<p>Позиция мыши: {{ x }}, {{ y }}</p>
</template>
<script setup>
import { useMouse } from './useMouse'
const { x, y } = useMouse()
</script>
useFetch
Получение данных с API:
// useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
error.value = null
try {
const response = await window.fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
fetch()
return { data, error, loading, refetch: fetch }
}
Использование:
<template>
<div v-if="loading">Загрузка...</div>
<div v-else-if="error">Ошибка: {{ error.message }}</div>
<div v-else>
<pre>{{ data }}</pre>
<button @click="refetch">Обновить</button>
</div>
</template>
<script setup>
import { useFetch } from './useFetch'
const { data, error, loading, refetch } = useFetch('/api/users')
</script>
useLocalStorage
Синхронизация с localStorage:
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// Читаем из localStorage при инициализации
const stored = localStorage.getItem(key)
if (stored) {
try {
value.value = JSON.parse(stored)
} catch (e) {
console.error('Error parsing localStorage value:', e)
}
}
// Сохраняем при изменении
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
Использование:
<template>
<input v-model="username" placeholder="Имя пользователя" />
<p>Сохранено: {{ username }}</p>
</template>
<script setup>
import { useLocalStorage } from './useLocalStorage'
const username = useLocalStorage('username', '')
</script>
useDebounce
Debounce для значения:
// useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timeoutId
watch(value, (newValue) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
Использование:
<template>
<input v-model="searchQuery" placeholder="Поиск..." />
<p>Поиск: {{ debouncedQuery }}</p>
</template>
<script setup>
import { ref } from 'vue'
import { useDebounce } from './useDebounce'
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
// Используйте debouncedQuery для API запросов
</script>
Composables с параметрами
Статические параметры
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
// ...
return { count, increment, decrement }
}
// Использование
const { count } = useCounter(10)
Реактивные параметры
// useFetch.js
import { ref, watch, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
try {
const response = await window.fetch(toValue(url))
data.value = await response.json()
} finally {
loading.value = false
}
}
// Следим за изменениями URL
watch(() => toValue(url), fetch, { immediate: true })
return { data, loading }
}
Использование:
<script setup>
import { ref } from 'vue'
import { useFetch } from './useFetch'
const userId = ref(1)
const url = computed(() => `/api/users/${userId.value}`)
const { data, loading } = useFetch(url)
// Автоматически перезагрузится при изменении userId
</script>
Композиция Composables
Composables можно комбинировать:
// useUser.js
import { useFetch } from './useFetch'
import { useLocalStorage } from './useLocalStorage'
export function useUser() {
const userId = useLocalStorage('userId', null)
const url = computed(() =>
userId.value ? `/api/users/${userId.value}` : null
)
const { data: user, loading, error } = useFetch(url)
function login(id) {
userId.value = id
}
function logout() {
userId.value = null
}
return {
user,
loading,
error,
login,
logout
}
}
Асинхронные Composables
// useAsyncData.js
import { ref } from 'vue'
export function useAsyncData(asyncFunction) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
asyncFunction()
.then(result => {
data.value = result
})
.catch(err => {
error.value = err
})
.finally(() => {
loading.value = false
})
return { data, error, loading }
}
Использование:
<script setup>
import { useAsyncData } from './useAsyncData'
const { data, error, loading } = useAsyncData(async () => {
const response = await fetch('/api/config')
return response.json()
})
</script>
Best Practices
Возвращайте refs, а не reactive
// Хорошо
export function useCounter() {
const count = ref(0)
return { count }
}
// Плохо
export function useCounter() {
const state = reactive({ count: 0 })
return state
}
Причина: refs сохраняют реактивность при деструктуризации.
Именование возвращаемых значений
// Хорошо - понятные имена
export function useMouse() {
const x = ref(0)
const y = ref(0)
return { x, y }
}
// Плохо - неясные имена
export function useMouse() {
const a = ref(0)
const b = ref(0)
return { a, b }
}
Cleanup в onUnmounted
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
Используйте toValue для гибкости
import { toValue } from 'vue'
export function useFetch(url) {
// url может быть ref, computed или обычным значением
const actualUrl = toValue(url)
// ...
}
Composables vs Mixins
Mixins (Vue 2)
// Плохо - mixins
const counterMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
Проблемы mixins:
- Неясный источник свойств
- Конфликты имён
- Неявные зависимости
Composables (Vue 3)
// Хорошо - composables
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
Преимущества composables:
- Явный источник
- Нет конфликтов имён
- Лучшая типизация
- Проще тестировать
Библиотеки Composables
VueUse
Коллекция готовых composables:
npm install @vueuse/core
<script setup>
import { useMouse, useLocalStorage, useFetch } from '@vueuse/core'
const { x, y } = useMouse()
const theme = useLocalStorage('theme', 'light')
const { data } = useFetch('/api/data').json()
</script>
Частые ошибки
Вызов composable вне setup
// Неправильно
export default {
data() {
const { x, y } = useMouse() // Ошибка!
return { x, y }
}
}
// Правильно
export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}
Условный вызов composable
// Неправильно
if (condition) {
const { count } = useCounter() // Ошибка!
}
// Правильно
const { count } = useCounter()
if (condition) {
// используйте count
}
Забыть cleanup
// Неправильно - утечка памяти
export function useMouse() {
const x = ref(0)
function update(event) {
x.value = event.pageX
}
window.addEventListener('mousemove', update)
// Забыли removeEventListener!
return { x }
}
// Правильно
export function useMouse() {
const x = ref(0)
function update(event) {
x.value = event.pageX
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x }
}
Вывод
Composables:
- Переиспользуемая логика с Composition API
- Лучше, чем mixins
- Именуются с префиксом
use - Возвращают refs
- Требуют cleanup в onUnmounted
- Можно комбинировать
- Поддерживают TypeScript
На собеседовании:
Важно уметь:
- Объяснить, что такое composables и зачем они нужны
- Показать примеры создания composables
- Рассказать о преимуществах перед mixins
- Объяснить best practices (именование, cleanup, возврат refs)
- Привести примеры популярных composables