Как превратить компонент в чистый UI-слой

от автора

в
Время чтения: 2 мин.

Со временем многие Vue-компоненты превращаются в мини-приложения. Они загружают данные, делают запросы, хранят бизнес-логику, управляют состоянием и одновременно отвечают за отображение.

Проблема становится заметна, когда один и тот же интерфейс нужно использовать в другом месте. Вместо переиспользования приходится копировать код или добавлять десятки пропсов и флагов.

В этой статье разберем подход, который помогает избежать подобных проблем — превращение компонента в чистый UI-слой.

Как выглядит проблемный компонент

Представим список заказов.

<script setup lang="ts">
const orders = ref([])
const loading = ref(false)

async function loadOrders() {
    loading.value = true

    orders.value = await api.orders.list()

    loading.value = false
}

async function cancelOrder(id: number) {
    await api.orders.cancel(id)

    await loadOrders()
}

onMounted(loadOrders)
</script>

<template>
    <div v-if="loading">
        Загрузка...
    </div>

    <div
        v-for="order in orders"
        :key="order.id"
    >
        <span>{{ order.name }}</span>

        <button
            @click="cancelOrder(order.id)"
        >
            Отменить
        </button>
    </div>
</template>

На первый взгляд всё хорошо.

Но компонент теперь отвечает сразу за несколько вещей:

  • загрузку данных;
  • отображение списка;
  • отмену заказа;
  • повторную загрузку данных;
  • состояние загрузки.

Из-за этого его сложно использовать повторно.

Выделяем ответственность

Первый шаг — понять, что компоненту на самом деле нужно для отображения.

Ему необходимы:

  • список заказов;
  • признак загрузки;
  • событие отмены заказа.

Всё остальное можно вынести наружу.

Создаем UI-компонент

Теперь компонент занимается исключительно отображением.

<script setup lang="ts">
defineProps<{
    orders: Order[]
    loading: boolean
}>()

defineEmits<{
    cancel: [number]
}>()
</script>

<template>
    <div v-if="loading">
        Загрузка...
    </div>

    <div
        v-for="order in orders"
        :key="order.id"
    >
        <span>{{ order.name }}</span>

        <button
            @click="$emit('cancel', order.id)"
        >
            Отменить
        </button>
    </div>
</template>

Компонент больше ничего не знает про API.

Он просто отображает данные и сообщает о действиях пользователя.

Переносим логику в контейнер

Теперь создадим отдельный компонент с логикой.

<script setup lang="ts">
import OrderList from './OrderList.vue'

const orders = ref([])
const loading = ref(false)

async function loadOrders() {
    loading.value = true

    orders.value =
        await api.orders.list()

    loading.value = false
}

async function cancelOrder(
    id: number
) {
    await api.orders.cancel(id)

    await loadOrders()
}

onMounted(loadOrders)
</script>

<template>
    <OrderList
        :orders="orders"
        :loading="loading"
        @cancel="cancelOrder"
    />
</template>

Теперь каждая часть занимается своей задачей.

Что мы получили

После разделения становится проще:

Тестировать

UI-компонент можно протестировать без моков API.

Достаточно передать данные через пропсы.

Переиспользовать

Один и тот же список можно использовать:

  • на странице заказов;
  • в модальном окне;
  • в административной панели;
  • в мобильной версии.

Компоненту всё равно, откуда пришли данные.

Поддерживать

Изменения в бизнес-логике не затрагивают отображение.

Изменения в интерфейсе не затрагивают работу API.

Следующий шаг — виджеты

Со временем контейнеры тоже начинают разрастаться.

Например:

OrderList
 └── OrderListWidget
      ├── загрузка данных
      ├── фильтрация
      ├── сортировка
      └── обработка действий

В такой схеме UI-компоненты остаются максимально простыми.

Они получают данные через пропсы и генерируют события.

Вся бизнес-логика живет в виджетах, composable-функциях или сервисах.

Как понять, что компонент пора разделять

Обычно это заметно по нескольким признакам:

  • внутри есть запросы к API;
  • используется несколько composable с бизнес-логикой;
  • компонент сложно протестировать;
  • компонент нельзя использовать без подключения половины приложения;
  • количество строк быстро растет.

Если компонент отвечает на вопрос «как показать?», он относится к UI-слою.

Если отвечает на вопрос «что делать?», скорее всего, это уже уровень логики.

Итог

Чистый UI-компонент не знает ничего о сервере, хранилищах данных или бизнес-процессах.

Его задача максимально проста:

  • получить данные через пропсы;
  • отобразить их;
  • сообщить о действиях пользователя через события.

Такой подход делает код более предсказуемым, упрощает тестирование и позволяет переиспользовать интерфейс без копирования логики между проектами и страницами.


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 4 + 7?