Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Композаблы: повторно используемая логика

Композабл (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>.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.