Композаблы: повторно используемая логика
Композабл (composable) — обычная функция JavaScript, которая использует ref, computed, watch и хуки и возвращает готовое состояние с функциями. Компоненты разбивают интерфейс на части, а композаблы — логику. По соглашению имя композабла начинается с use.
Где хранить
Композаблы кладут в папку src/composables/. Её нет в стартовом проекте — создайте её и файл src/composables/useTodos.js:
// src/composables/useTodos.js
import { ref, computed } from 'vue'
export function useTodos() {
const todos = ref([])
let nextId = 1
const remaining = computed(() => todos.value.filter((t) => !t.done).length)
function addTodo(text) {
todos.value.push({ id: nextId++, text, done: false })
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id)
}
return { todos, remaining, addTodo, removeTodo }
}Это вся логика списка задач из предыдущих уроков, только без шаблона. Функция возвращает объект с ref, вычисляемым свойством и двумя функциями.
Использование в компоненте
<!-- src/App.vue -->
<script setup>
import { useTodos } from './composables/useTodos.js'
import TodoForm from './components/TodoForm.vue'
import TodoItem from './components/TodoItem.vue'
const { todos, remaining, addTodo, removeTodo } = useTodos()
</script>
<template>
<TodoForm @add="addTodo" />
<ul>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@remove="removeTodo(todo.id)"
/>
</ul>
<p>Осталось: {{ remaining }}</p>
</template>Список работает как раньше, но App.vue стал короче: в нём только связь между композаблом и компонентами. Деструктуризация здесь безопасна — todos и remaining остаются ref-объектами.
Композабл с хуками
Внутри композабла можно вызывать хуки жизненного цикла — они привяжутся к компоненту, который вызвал функцию. Создайте src/composables/useNow.js:
// src/composables/useNow.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useNow() {
const now = ref(new Date())
let timerId
onMounted(() => {
timerId = setInterval(() => (now.value = new Date()), 1000)
})
onUnmounted(() => clearInterval(timerId))
return now
}<!-- src/components/TodoClock.vue -->
<script setup>
import { useNow } from '../composables/useNow.js'
const now = useNow()
</script>
<template>
<p>Сейчас {{ now.toLocaleTimeString() }}</p>
</template>Компонент часов из урока про хуки уменьшился до трёх строк скрипта, а таймер по-прежнему останавливается при удалении компонента.
Своё состояние или общее
Каждый вызов useTodos() создаёт новый ref, поэтому у двух компонентов будут два разных списка. Если нужен один список на всё приложение, объявите ref вне функции, на уровне модуля, или используйте хранилище Pinia — оно разобрано в отдельной главе.
Частая ошибка
Вызывать композабл не в <script setup>, а внутри обработчика или setTimeout, например function onClick() { const now = useNow() }. Хуки внутри композабла не найдут компонент, к которому привязаться, и Vue выведет предупреждение, а таймер никогда не остановится. Вызывайте композаблы синхронно на верхнем уровне <script setup>.