Как объявляется состояние компонента: props, локальные данные и emits?

JuniorVue.js #vue #state #data

Вопрос

Какие есть способы объявить и передать состояние компонента — props, локальные данные, emits — и как они соотносятся друг с другом?

Короткий ответ

Props — это данные, которые родитель передаёт дочернему компоненту сверху вниз; они только для чтения внутри дочернего компонента. Локальное состояние объявляется через ref/reactive внутри <script setup> и принадлежит самому компоненту. Emits — это механизм, которым дочерний компонент сообщает родителю о событиях (снизу вверх), не изменяя его данные напрямую.

Подробный ответ

Vue-компонент — это изолированный кусок UI с собственными входами и выходами:

  • Props (входные данные). Объявляются через defineProps(). Поток данных — однонаправленный: родитель владеет значением и передаёт его вниз через атрибут (:count="value"). Дочерний компонент не должен мутировать проп напрямую — это считается антипаттерном и Vue выдаст предупреждение в dev-режиме. Если пропу нужно локальное изменяемое отражение, для этого заводят отдельный ref, инициализированный значением пропа (или используют defineModel() для двусторонней привязки).
  • Локальное состояние. Данные, которые нужны только внутри компонента и не передаются извне, объявляются через ref() (для примитивов и вообще любых значений) или reactive() (для объектов). Это состояние живёт и умирает вместе с экземпляром компонента.
  • Emits (исходящие события). Поскольку проп менять напрямую нельзя, для связи «снизу вверх» дочерний компонент эмитит событие через defineEmits(), а родитель подписывается на него через @event-name и уже сам обновляет своё состояние. Это делает поток данных предсказуемым: всегда понятно, откуда пришло изменение.

Такая модель называется «props down, events up» — данные текут вниз по дереву компонентов, а изменения поднимаются наверх через события, вместо прямой мутации чужого состояния.

Пример

<!-- CounterButton.vue (дочерний компонент) -->
<script setup>
const props = defineProps({
  modelValue: { type: Number, required: true }
})
const emit = defineEmits(['update:modelValue'])

function increment() {
  // проп не мутируется напрямую — эмитим событие с новым значением
  emit('update:modelValue', props.modelValue + 1)
}
</script>

<template>
  <button @click="increment">+1 (сейчас: {{ modelValue }})</button>
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import CounterButton from './CounterButton.vue'

// локальное состояние родителя
const count = ref(0)
</script>

<template>
  <!-- v-model — сахар над :modelValue + @update:modelValue -->
  <CounterButton v-model="count" />
  <p>Значение в родителе: {{ count }}</p>
</template>

Дополнительные вопросы

  • Почему нельзя напрямую мутировать проп внутри дочернего компонента и что произойдёт, если попытаться?
  • Чем ref отличается от reactive при объявлении локального состояния?
  • Как передать несколько именованных значений через v-model (v-model:title, v-model:count)?