← Курс/Composition API: setup() и логическое разделение кода#233 из 257+30 XP

Composition API: setup() и логическое разделение кода

Зачем появился Composition API

В больших компонентах с Options API код одной фичи разбросан по разным секциям (data, methods, computed, watch). Это затрудняет чтение и переиспользование логики.

// Options API — логика фичи «поиск» разбросана по всему компоненту
export default {
  data() {
    return {
      // данные поиска
      searchQuery: '',
      searchResults: [],
      searchLoading: false,
      // данные пагинации
      currentPage: 1,
      totalPages: 0,
    }
  },
  methods: {
    // методы поиска
    async doSearch() { /* ... */ },
    clearSearch() { /* ... */ },
    // методы пагинации
    nextPage() { /* ... */ },
    prevPage() { /* ... */ },
  },
  watch: {
    searchQuery: 'doSearch',   // наблюдение за поиском
    currentPage: 'doSearch',   // наблюдение за пагинацией
  }
}

setup() — сердце Composition API

setup() запускается до монтирования компонента. Всё, что возвращается из setup, доступно в шаблоне:

import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    // Состояние (аналог data)
    const count = ref(0)
    const name = ref('Vue')

    // Вычисляемые значения (аналог computed)
    const doubled = computed(() => count.value * 2)
    const greeting = computed(() => `Hello, ${name.value}!`)

    // Методы (аналог methods)
    function increment() {
      count.value++
    }

    // Lifecycle (аналог mounted)
    onMounted(() => {
      console.log('Компонент смонтирован')
    })

    // Наблюдатели (аналог watch)
    watch(count, (newVal, oldVal) => {
      console.log(`count изменился: ${oldVal}${newVal}`)
    })

    // Возвращаем что нужно в шаблоне
    return { count, name, doubled, greeting, increment }
  }
}

Сравнение: Options API → Composition API

// OPTIONS API
export default {
  data() { return { count: 0 } },
  computed: {
    doubled() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  },
  mounted() {
    console.log('ready')
  }
}

// COMPOSITION API (эквивалент)
import { ref, computed, onMounted } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
    const increment = () => count.value++
    onMounted(() => console.log('ready'))
    return { count, doubled, increment }
  }
}

script setup — упрощённый синтаксис

<!-- Не нужен явный return — всё автоматически доступно в шаблоне -->
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)
const increment = () => count.value++
</script>

Composables — логические блоки

Главное преимущество Composition API — возможность вынести логику в **composables** (функции use*):

// useSearch.js — вся логика поиска в одном месте
export function useSearch() {
  const query = ref('')
  const results = ref([])
  const loading = ref(false)

  async function search() {
    if (!query.value) return
    loading.value = true
    results.value = await fetchSearch(query.value)
    loading.value = false
  }

  return { query, results, loading, search }
}

// Компонент — чистый и читаемый
export default {
  setup() {
    const { query, results, loading, search } = useSearch()
    const { currentPage, nextPage } = usePagination(results)
    return { query, results, loading, search, currentPage, nextPage }
  }
}

Примеры

Composable useCounter через замыкание — аналог Composition API в чистом JS

// Composable через замыкание — аналог ref + методов в Vue 3
function useCounter(initialValue = 0, { min = -Infinity, max = Infinity } = {}) {
  // Закрытое состояние (аналог ref внутри composable)
  let count = initialValue

  function clamp(value) {
    return Math.min(max, Math.max(min, value))
  }

  return {
    // Геттер (аналог count.value)
    get value() { return count },

    // Методы (аналог функций, возвращаемых из setup)
    increment(step = 1) {
      count = clamp(count + step)
      return this
    },
    decrement(step = 1) {
      count = clamp(count - step)
      return this
    },
    reset() {
      count = initialValue
      return this
    },
    set(value) {
      count = clamp(value)
      return this
    },
  }
}

// Аналог использования в компоненте:
// const { count, increment, decrement } = useCounter(0, { min: 0, max: 10 })
const counter = useCounter(0, { min: 0, max: 10 })

console.log(counter.value)    // 0
counter.increment()
counter.increment()
counter.increment(3)
console.log(counter.value)    // 5

counter.increment(100)        // зажато на max=10
console.log(counter.value)    // 10

counter.decrement(3)
console.log(counter.value)    // 7

counter.reset()
console.log(counter.value)    // 0

// Можно комбинировать composables — как в Vue setup()
function useToggle(initialValue = false) {
  let state = initialValue
  return {
    get value() { return state },
    toggle() { state = !state; return this },
    setTrue() { state = true; return this },
    setFalse() { state = false; return this },
  }
}

// "Компонент" — комбинирует composables как в setup()
function createComponent() {
  const counter = useCounter(0, { min: 0, max: 5 })
  const isVisible = useToggle(true)

  return {
    get count() { return counter.value },
    get visible() { return isVisible.value },
    increment: () => counter.increment(),
    toggleVisibility: () => isVisible.toggle(),
    render() {
      return isVisible.value
        ? `Счётчик: ${counter.value}`
        : '(скрыто)'
    }
  }
}

const comp = createComponent()
console.log(comp.render())    // Счётчик: 0
comp.increment()
comp.increment()
console.log(comp.render())    // Счётчик: 2
comp.toggleVisibility()
console.log(comp.render())    // (скрыто)