← Курс/Компоненты Vue: defineComponent и Props#220 из 257+30 XP

Компоненты Vue: defineComponent и Props

Что такое компонент

Компонент — это переиспользуемый, самодостаточный блок интерфейса. В Vue 3 каждый компонент объединяет шаблон, логику и стили. Компоненты позволяют разбить сложный UI на маленькие, понятные части.

defineComponent

defineComponent() — вспомогательная функция Vue 3 для определения компонента с поддержкой TypeScript-вывода типов:

// Button.vue (упрощённо — SFC)
import { defineComponent } from 'vue'

const Button = defineComponent({
  name: 'Button',
  props: {
    label: String,
    disabled: Boolean,
  },
  setup(props) {
    return () => `<button disabled=${props.disabled}>${props.label}</button>`
  }
})

Props — входные данные компонента

Props — это параметры, которые родитель передаёт дочернему компоненту. Данные текут **только сверху вниз** (однонаправленный поток).

Объявление props с валидацией

defineComponent({
  props: {
    // Просто тип
    title: String,

    // Объект с опциями
    count: {
      type: Number,
      required: true,         // обязателен
    },

    status: {
      type: String,
      default: 'active',      // значение по умолчанию
      validator(value) {      // кастомная валидация
        return ['active', 'inactive', 'pending'].includes(value)
      }
    },

    // Несколько допустимых типов
    id: [String, Number],
  }
})

Однонаправленный поток данных

Props идут от родителя к ребёнку. Дочерний компонент **не должен изменять** props напрямую — это нарушение однонаправленного потока:

setup(props) {
  // НЕПРАВИЛЬНО — мутировать props
  // props.count = 10

  // ПРАВИЛЬНО — создать локальное состояние на основе props
  const localCount = ref(props.count)
  return { localCount }
}

Default values и required

props: {
  message: {
    type: String,
    required: true,           // Vue выдаст предупреждение если не передан
  },
  theme: {
    type: String,
    default: 'light',         // используется если prop не передан
  },
  items: {
    type: Array,
    default: () => [],        // для объектов/массивов — функция-фабрика!
  }
}

Использование компонента

<!-- Родительский шаблон -->
<template>
  <Button label="Нажми меня" :disabled="false" />
  <UserCard :user="currentUser" status="active" />
</template>

Примеры

Паттерн компонентов через factory functions с валидацией props

// Аналог defineComponent через factory function
function defineComponent({ name, props: propsSchema, setup }) {
  return function createInstance(props) {
    // Валидация props
    for (const [key, schema] of Object.entries(propsSchema)) {
      const value = props[key]
      const def = typeof schema === 'function' ? { type: schema } : schema

      // Проверка required
      if (def.required && (value === undefined || value === null)) {
        console.warn(`[Component: ${name}] Prop "${key}" is required but not provided`)
        continue
      }

      // Если значение не передано — применить default
      if (value === undefined && 'default' in def) {
        props[key] = typeof def.default === 'function' ? def.default() : def.default
      }

      // Проверка типа
      if (value !== undefined && def.type) {
        const expectedType = def.type.name
        const actualType = Array.isArray(value) ? 'Array' : typeof value
        const typeMatch =
          actualType === expectedType.toLowerCase() ||
          value instanceof def.type
        if (!typeMatch) {
          console.warn(`[Component: ${name}] Prop "${key}": expected ${expectedType}, got ${actualType}`)
        }
      }

      // Кастомный validator
      if (value !== undefined && def.validator) {
        if (!def.validator(value)) {
          console.warn(`[Component: ${name}] Prop "${key}": validator failed for value "${value}"`)
        }
      }
    }

    // Запускаем setup с провалидированными props
    return setup(props)
  }
}

// Создаём компонент StatusBadge
const StatusBadge = defineComponent({
  name: 'StatusBadge',
  props: {
    label: {
      type: String,
      required: true,
    },
    status: {
      type: String,
      default: 'active',
      validator: (v) => ['active', 'inactive', 'pending'].includes(v),
    },
    count: {
      type: Number,
      default: 0,
    },
  },
  setup(props) {
    return {
      render() {
        return `[${props.status.toUpperCase()}] ${props.label} (x${props.count})`
      }
    }
  }
})

// Использование
const badge1 = StatusBadge({ label: 'Tasks', status: 'active', count: 5 })
console.log(badge1.render())
// [ACTIVE] Tasks (x5)

const badge2 = StatusBadge({ label: 'Messages' }) // defaults применятся
console.log(badge2.render())
// [ACTIVE] Messages (x0)

// Невалидный статус — будет предупреждение
const badge3 = StatusBadge({ label: 'Alerts', status: 'broken', count: 1 })
console.log(badge3.render())