Привет, Хабр! В этой статье разберемся, как frontend-разработчику готовить на «кухне» props. Выбирайте подходящий уровень сложности: джуны-поварята смогут лучше разобраться в работе и применении props на фреймворке Vue.js, а еще мы затронем тему валидации. Для мидлов и более опытных специалистов — настоящих шеф-поваров мы приготовили продвинутые кулинарные техники props, где можно освежить в памяти некоторые детали или решить проектную проблему, если замылился глаз. 

Props – от слова «properties» (здесь и дальше будем использовать слово «props») – это специальные атрибуты, используемые в экосистеме Vue для передачи данных в компоненты. Они являются частью системы реактивности, позволяют определять типы данных и проводить с ними валидацию.

По большей части мы будем покрывать основы передачи props. Поэтому статья будет актуальна как для Vue 2, так и для Vue 3, поскольку по части объявления props отличий мало. Но так как в Composition API и TypeScript все же они есть, то мы рассмотрим их тоже.

Если вы готовы, то добро пожаловать, мы начинаем наш кулинарный гайд! Bon appétit!

Оглавление

  1. Разбираемся с Props (уровень сложности: 1/5)

  2. Что можно добавить в props и не испортить всё

    Как передавать props в компонент?

  3. Валидация props (уровень сложности: 3/5)

    О передаваемых типах данных

    Default – значения по умолчанию

    Required – обязательные значения

    Validator – своя валидация

    Проблемы с реактивностью props в объектах или массивах

    Про передачу функций в качестве props

    Есть ли смысл в одновременной передаче default и required?

  4. Продвинутые кулинарные техники props (уровень сложности: 4/5)

    Хуки во Vue

    Что такое принцип одностороннего потока данных (One way data flow)?

    Что такое props drilling? 

    Когда использовать props, а когда Vuex getters?

  5. Заключение

Разбираемся с props

Уровень сложности: 1/5

Выше мы уже дали определение, а теперь разберемся с props на простом примере.

Допустим, у нас есть сайт пиццерии, на котором мы отображаем… пиццу. 

<template>
  <div>
    <h2>Pizza 1</h2>
    <ul>
      <li>Crust: Thin</li>
      <li>Sauce: Marinara</li>
      <li>Cheese: Mozzarella</li>
      <li>Toppings: Pepperoni, Mushrooms, Green Peppers</li>
    </ul>
    <h2>Pizza 2</h2>
    <ul>
      <li>Crust: Thick</li>
      <li>Sauce: BBQ</li>
      <li>Toppings: Chicken, Bacon, Onions</li>
    </ul>
  </div>
</template>

Код выше является не лучшим примером, поскольку он не переиспользуемый, содержит захардкоженные значения и сложно масштабируется, а также изменяется в будущем. Однако во Vue мы можем выделить повторяющуюся логику в отдельный компонент и передавать ему данные через props. Как мы уже говорили выше, props — это специальные атрибуты, которые передают данные в компоненты и являются частью системы реактивности Vue. А она, в свою очередь, позволяет создавать сложные и переиспользуемые компоненты. Props и компоненты отлично работают вместе как лучшие друзья, как желтый и красный из эмемдемс, как пельмени и сметана, как пицца и ананасы, как котлетки с пюрешкой – идут вместе и прекрасно дополняют друг друга! =)

Вкус котлеток с пюрешкой. Сгенерировано нейросетью Kandinsky 2.2
Вкус котлеток с пюрешкой. Сгенерировано нейросетью Kandinsky 2.2
<template>
  <div>
    <h2>{{ title }}</h2>
    <ul>
      <li>Crust: {{ crust }}</li>
      <li v-if="sauce">Sauce: {{ sauce }}</li>
      <li v-if="cheese">Cheese: {{ cheese }}</li>
      <li>Toppings:</li>
      <ul>
        <li v-for="topping in toppings" :key="topping">{{ topping }}</li>
      </ul>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    },
    isThick: {
      type: Boolean,
      default: true
    },
    sauce: {
      type: String,
      default: null
    },
    cheese: {
      type: String,
      default: “
    },
    toppings: {
      type: Array,
      required: true
    }
  }
}
</script>

Мы создали компонент Pizza, который принимает 5 props: “title”, “crust”, “sauce”, “cheese”, “toppings”. Теперь можем вставить его в нужное место и передавать в него значения нужных нам props. В компоненте мы можем использовать props внутри шаблона <template>, используя синтаксис {{ }}. Например, если у вас есть props с именем “crust”, вы можете отобразить его значение как {{ crust }}.

То есть вы можете использовать props так же, как и переменные, указанные в data, которые просто передаются из родительского компонента. Но важно иметь в виду, что props должны обрабатываться как данные, доступные только для чтения, и не должны изменяться непосредственно в дочернем компоненте. 

Если нам потребуется изменить значения этих props, мы будем изменять значение в родительском компоненте. Если необходимо изменить стили, передать новые props, изменить логику – мы просто внесем эти изменения в компонент Pizza! Chefs kiss ????!

<template>
  <div>
    <Pizza title="Pizza 1" :is-thick=”false” sauce="Marinara" cheese="Mozzarella" :toppings="['Pepperoni', 'Mushrooms', 'Green Peppers']" />
    <Pizza :title="'Pizza 2'" is-thick :sauce="'BBQ'" :toppings="['Chicken', 'Bacon', 'Onions']" />
    <Pizza title="Pizza 3" crust="Thin" :toppings="['Pepperoni']" />
  </div>
</template>

<script>
import Pizza from '@/components/Pizza.vue'

export default {
  components: {
    Pizza
  }
}
</script>

Кто молодец и заметил все отличия? Ради примера представим передачу props по-разному, чтобы показать некоторые отличия. 

Заострим внимание на title="Pizza 1" и :title="'Pizza 2'". В первом компоненте мы передаем String значение “Pizza 1” как статичное значение, во втором мы передаем значение “Pizza 2” как динамическое (реактивное) через директиву v-bind: (или сокращенно : ). 

Практически всегда вы захотите посылать props именно через сокращенный “v-bind:” (как во втором компоненте) – так можно передавать сложные выражения, а также избавить себя и других от внезапных ошибок в будущем. Статический подход используется, когда передаваемое значение не изменяется и не изменится, и применяется со String- и Boolean-значениями. Интересный нюанс с передачей Boolean:

<PizzaItem is-hawaiian />

<PizzaItem :is-hawaiian="true" />

В обоих компонентах передается одинаковое значение “true” – и такой подход используется довольно часто, чтобы держать код более ясным и кратким.

Что можно добавить в props и не испортить всё

Представьте пиццу, в которую вы можете добавить какой угодно ингредиент и получить на выходе соответствующий вариант! В одну пиццу в качестве соуса мы добавим томатную пасту, в другую – соус, а в третьей обойдемся без него. 

Props позволяют нам передавать данные из родительского компонента в дочерний. Их  можно использовать для отправки данных любого типа, включая строки, числа, объекты, массивы и даже функции. Они используются для изменения поведения дочернего компонента и для изменения отображаемых данных. Props реагируют на изменение данных в родительском компоненте, меняясь, и меняя данные в используемом компоненте. Передавая данные через props, мы можем создавать повторно используемые модульные компоненты, которые можно легко настроить/переиспользовать (что, как мы помним, следует принципу DRY – Don't Repeat Yourself!)  и адаптировать к различным вариантам использования, делая наш код более гибким, адаптивным и… переиспользуемым.

Как передавать props в компонент?

Конечно же, можно передавать props просто и так... 

props: {
    name: String,
    description: String,
    price: Number
}

...или таким образом...

props: ['pizzaName', 'slicesCount']

...что очень напоминает эту «пиццу»:

Почему так лучше не делать?

  1. Важно указывать типы и значения по умолчанию для каждого из props, что позволит легко отслеживать ошибки и быстро их исправлять. 

  2. При увеличении количества props, возможно, будет сложнее запомнить, какие из них обязательны, а какие нет, и какие значения они могут иметь. Чтобы избежать этой проблемы, необходимо явно указывать параметры для каждого props. Таким образом, вы сможете легко поддерживать и изменять свой код по мере его развития.

  3. Примером хорошего использования props является объявление их в качестве объекта с указанием его валидации. Название указывается всегда в camelCase (например, pizzaName) – это стандарт объявления данных, указанный в документации и используемый популярными библиотеками компонентов.

Существует 4 типа валидаций. Каждый из них мы рассмотрим далее в статье.

1) type – проверка типа данных значения, то есть, String, Number, Boolean, Object, Array, Date, Function. Еще могут быть конструкторы, но пока, для понимания материала, сосредоточимся на этом.

2) default – значение по умолчанию, если из родителя не передалось значение или имеет undefined значение.

3) required – проверка, является ли значение не undefined. Выдаст ошибку, если значение не было передано.

4) validator – проверка значения через настраиваемые условия. Тоже выдаст ошибку, если значение не пройдет проверку.

Обычно не указывают все 4 валидатора сразу, но нужно знать, что хорошей практикой является обязательное указание типа передаваемого props, а default/required (хотя бы что-то одно из этого крайне предпочтительно).

Давайте создадим список из props. Пока мы сделаем его очень простым, но по мере продвижения по статье этот код будет обрастать новыми валидациями и дополнениями. Можно проспойлерить и сразу посмотреть на готовый код в конце раздела о валидации или через поиск по странице «финальный код».

props: {
    // String
    pizzaName: {
      type: String,
    },

    // Number
     slicesCount: {
      type: Number,
    },

    // Boolean
    isHawaiian : {
      type: Boolean,
    },

    // Array
    toppings: {
      type: Array,
    },

    // Object
    pizzaItem: {
      type: Object,
    },
    
// Даты
delieveryDate: {
  type: Date, 
},

 // Пример props с любым типом, лишь бы не undefined
   definedProp: {
     required: true,
   },
};

И вместо первого варианта пиццы у нас получится вот такой:

Уже лучше ????  Сгенерировано нейросетью Kandinsky 2.2
Уже лучше ???? Сгенерировано нейросетью Kandinsky 2.2

Vue 3, Composition API, TypeScript и props

Во Vue 3 появился Composition API, отличающийся от привычного синтаксиса Vue 2 (теперь – Options API), он более краткий, удобный и лаконичный. Также его добавили «на пробу» для Vue 2.17, но это не совсем то.

Что касается props, то в компонентах Vue 3 с объявленным <script> все будет аналогично. Мы также получаем доступ к props через setup:

props: {
   pizzaName: {
      type: String,
       default: "",
 },
 },
 setup(props) {
   console.log(props.pizzaName)
 }   

В компонентах Vue 3 с <script setup> мы передаем props в компонент так же, но их объявление внутри компонента отличается, поэтому используем defineProps так:

<script setup>
const props = defineProps({
  pizzaName: {
    type: String,
    default: "",
  },
});
</script>

Или можем применить более краткий синтаксис без привязки к переменной const props, обращаясь к передаваемому значению напрямую по названию: 

<script setup>
const props = defineProps({
  pizzaName: {
    type: String,
    default: "",
  },
});
</script>

Если мы используем в компоненте <script setup lang="ts"> – TypeScript, то кроме обычного JS объявления передаваемых значений (как в примерах выше) можно сделать таким образом:

const props = defineProps<{
  pizzaName: string
  slicesCount?: number
}>()

Props и interface

Для чего используются interface в связке с props? Для создания набора правил, как должны выглядеть передаваемые props –  элегантно! Мы указываем все, что хотим от props в interface, и можно (и даже нужно) переиспользовать их через импорт.

Как всегда, плюсы interface  – улучшает читаемость кода, помогает с поддержкой кода спустя длительное время и позволяет другим разработчикам быстро войти в курс дела.

interface Pizza {
  pizzaName: string
  slicesCount?: number
}
const props = defineProps<Pizza>()

Деструктуризация props в Vue 3.3

В версии Vue 3.3 завезли много фич, в том числе и экспериментальные, но нас в рамках этой статьи интересует одна – деструктуризация props и сохранение реактивности. Если бы вы попробовали так сделать ранее, то потеряли бы реактивность этого значения, но сейчас вы можете деструктурировать данные значения, чтобы спокойно их использовать. Выглядеть это будет так:

<script setup lang="ts">
const { pizzaName } = defineProps<{
    pizzaName: string}>()

//Любые функции могут использовать pizzaName 
</script>

<template>
  <div>
    Название: {{ pizzaName }}
  </div>
</template>

Валидация props

Уровень сложности: 3/5

Настройка валидаторов для props может быть полезна по нескольким причинам:

1. Обнаружение ошибок на ранней стадии. Средства проверки помогают обнаруживать ошибки во время разработки, проверяя значения props, передаваемые из родительского компонента. Если значение props не соответствует критериям проверки, Vue выдаст предупреждение. Это может помочь вам выявить и устранить проблемы на ранней стадии, что приведет к созданию более надежных и безошибочных компонентов.

2. Четкая документация. Валидаторы могут служить формой документации для других разработчиков, работающих с вашими компонентами. Указывая ожидаемый тип, форму или ограничения props, вы облегчаете другим разработчикам понимание того, как правильно использовать ваш компонент.

3. Удобство сопровождения кода. Валидаторы позволят сохранить целостность ваших компонентов, гарантируя, что последние получают правильные данные. Это может предотвратить неожиданное поведение, сделав вашу кодовую базу более удобной для обслуживания и упрощающей рефакторинг.

4. Улучшенный пользовательский интерфейс. Проверяя значения prop и выявляя ошибки на ранней стадии, вы можете предотвратить потенциальные проблемы, которые могут привести к ухудшению пользовательского интерфейса, такие как неработающие макеты или отображение неправильных данных.

О передаваемых типах данных

Представим, что вы собрались готовить блюдо. Мы берем рецепт и следуем ему. Хотим сделать блинчики – берем муку, молоко, соль, сахар, яйца – казалось, бы все очевидно? Если мы тщательно следуем рецепту, то получаем вкусные блины, прямо как на картинке. Но если нарушаем рецепт и делаем по-своему, то блины подгорают, рвутся, получаются безвкусными и так далее.

Аналогично и с props: если мы вводим типизацию и валидацию, то больше шансов, что мы заметим ошибку быстрее и сможем ее легко исправить, а ошибки в консоли нас направят в этом направлении. Ах, если бы с блинами было так же :(

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

Можно ли указывать несколько видов типов?

Да, такая возможность предусмотрена, но количество сценариев, когда это оправдано, строго ограничено. «Это позволяет коду быть более гибким!», – это оправдание разработчиков, которые при использовании TypeScript указывают везде тип “Any”.  И чаще всего это означает, что настало время для рефактора и пересмотра использования данного компонента.

Можно указывать тип props [String, Number], но это открывает новый простор для ошибок. Можем представить первую же, когда разработчик попробует вызвать .split на строке, но окажется что было передано число.

Или помните, как выше мы рассматривали динамическую и статическую передачу данных? Если мы передаем :is-hawaiian="true" – это Boolean, а если is-hawaiian="true", то это уже значение String. Правда, если есть хорошо настроенный линтер, и вы хотя бы пишете код не в блокноте, то эта ошибка будет подсвечена. Как видите, легко запутаться, поэтому лучше указывать лишь один тип props, либо добавлять валидацию, чтобы по ошибкам в консоли, мы могли сразу понять, где же совершили ошибку.

Отдельного упоминания стоит Boolean значения и их очередность в мульти-типах.

Если мы передаем в компоненте

<MyComponent disabled />

и объявляем значение внутри как

 props: {
    disabled: [String, Boolean]
  }

то вместо ожидаемого disabled === true мы получим disabled === ‘’. Но если мы объявим значение как 

 props: {
    disabled: [Number, Boolean]
  }

то получим корректное значение disabled === true

Чувствуете, как вам уже хочется указывать только один тип передаваемого значения?

Default – значения по умолчанию

Представим: мы готовим шаурму, собираем и выкладываем все необходимые ингредиенты, и (внезапно) понимаем, что у нас кончились листья салата. Мы лезем в холодильник и берем китайскую капусту, заменяя ею недостающий ингредиент… Шаурма спасена!

Точно так же работают значения по умолчанию во Vue: они позволяют подставлять выбранное значение, если это значение не было передано из родительского компонента, что помогает избежать ненужных ошибок или неожиданного поведения в вашем приложении. 

Но не всегда данное поведение связано с ошибками. Например, если у нас есть переиспользуемый компонент, который используется в 50 местах в коде, и в 40 из них мы используем одно и то же значение, мы можем указать это значение по умолчанию и не передавать его в 40 случаях. Вместо этого значение будет использоваться из значения по умолчанию, которое мы указали.

Или же вернемся к примеру с шаурмой – мы ОЧЕНЬ хотим листья салата, поэтому заказываем доставку курьером. Если курьер пришел, мы используем этот ингредиент. Если курьер по пути заблудился, лишив нас заказа, что ж, настало время капусты. 

Мы можем посылать запрос на бэк за данными, и пока их нет, использовать значения по умолчанию, для отображения данных пользователю.

Boolean-значения по умолчанию равны false (естественно), возникает вопрос – нужно ли указывать для них default: false? С одной стороны, мы улучшаем читаемость кода, следуя единому стилю объявления props, а также избегаем проблем с линтером в будущем. С другой стороны, Boolean вообще не требует default-значения, так как оно либо false, если не указано, и true, если указано.

Required – обязательные значения

Но иногда заменить какой-либо ингредиент нельзя или не имеет смысла. Без чего не выйдет шаурма? Без лаваша. Вы же не станете готовить шаурму, когда у вас нет лаваша? Конечно, нет! Вот тут-то и вступают в игру требуемые значения.

Обязательными являются значения, необходимые для корректной работы компонента. К примеру, если мы отрисовываем в компоненте какой-либо список значений, которые передаем из родителя, то будет неплохой идеей указать required. В таком случае, если мы не смогли передать этот список, сразу увидим ошибку. Поэтому хорошим решением является проверка присутствия этих значений через v-if в родительском компоненте. Также важно соблюдать баланс и не увлекаться required-значениями, так как переизбыточное количество required props делают код сложно поддерживаемым. 

Вернемся к нашему коду, дополнив его валидацией default и required.

 props: {
    // String
    pizzaName: {
      type: String,
      required: true,
      default: 'Моцарелла',
    },

    // Number
    slicesCount: {
      type: Number,
      default: 4,
    },

    // Boolean
    isHawaiian : {
      type: Boolean,
      default: false,
    },

    // Array
    toppings: {
      type: Array,
      required: true,
    },

    // Object
    pizzaItem: {
      type: Object,
      required: true,
    },
};

Validator – своя валидация

Когда мы покупаем продукты по списку рецепта, то проверяем каждый ингредиент. Для пиццы нужно тесто – на выбор любое из трех. Соус? Хочу только чесночный и кетчуп, любой другой – мимо. Колбаса – проверяем срок годности. Томаты – «фрукты» в моей пицце? Бррр!

Validator – более тщательная проверка каждого передаваемого значения. Она совсем не обязательна. Но это еще один способ сказать другому разработчику, что мы хотим видеть в этом передаваемом значении, и возможность понять, что же пошло не так. Это еще один способ узнать, что мы что-то сломали, и не плодить инциденты на проекте.

Ниже пример для проверки входящего значения на один из доступных вариантов. Новому разработчику будет ясно, что в компонент можно передавать одно из этих значений:

	shawermaSize: {
      validator: function(value) {
        return ["normal", "xxl", "king", "adultStar"].includes(value);
      }
    },

Бонусом идет проверка на очепятки.

Пример валидации каждого объекта в массиве на соответствие классу:

  validator: (dishes) => {
     return dishes.every((dish) => dish instanceof Dish);
   },

Проблемы с реактивностью props в объектах или массивах

При использовании объектов или массивов в качестве default-значений для props вы можете столкнуться с проблемой реактивности, когда изменения в объекте или массиве не обнаруживаются системой реактивности Vue. Это связано с тем, что система реактивности Vue находит только изменения, внесенные непосредственно в объект или массив, а не те, которые находятся в его свойствах или элементах.

Чтобы решить данную проблему, вы можете использовать функцию для возврата нового объекта или массива в качестве default-значения для props вместо использования объекта или массива. Это гарантирует, что новый объект или массив создается каждый раз при создании экземпляра компонента, поэтому изменения, внесенные в объект или массив, будут обнаружены системой реактивности Vue.

Ниже пример того, как использовать функцию для возврата нового объекта в качестве значения по умолчанию для prop:

props: {
  myObject: {
    type: Object,
    default: function () {
      return { prop1: 'default value 1', prop2: 'default value 2' }
    }
  }
}

Как использовать функцию для возврата нового массива в качестве значения по умолчанию для prop:

props: {
  myArray: {
    type: Array,
    default: function () {
      return ['default value 1', 'default value 2']
    }
  }
}

Используя функцию для возврата нового объекта или массива в качестве значения по умолчанию для prop, вы можете гарантировать, что изменения, внесенные в объект или массив, будут обнаружены системой реактивности Vue.

Про передачу функций в качестве props

В качестве props можно передавать и функции. Для пользователей React – это вполне норма и обычный подход. Для Vue – это анти-паттерн, так как существуют emits (передача событий из дочернего в родительский компонент), что может привести к некорректному обновлению компонента, плохой реактивности и часам работы в поисках ответа на вопрос «почему оно не работает?». Но, допустим, нам надо, тогда:

logItemById(id) {
      console.log(id)
 }

Передаем эту функцию в дочерний компонент:

<child-component v-for="item in items" :key="item.id" :item="item" 
:onLog="logItemById" />

Теперь мы можем вызывать этот метод изнутри данного компонента: 

<button @click="onLog(item.id)">Отправить ID</button>

Если перешли из React, то лучше обратите внимание на использование emits

Есть ли смысл в одновременной передаче default и required?

По умолчанию у всех props значение required = false. Если мы указываем true, то родительский компонент обязан предоставить это значение, значит его нельзя заменить каким-то другим пустым.

И технически, если мы добавляем default-значение, то требование required уже считается выполненным, ведь значение у нас уже есть и оно подставится, компонент отрисуется. Но ошибка в консоли останется висеть, если мы не передадим это значение.

Однако попробуем представить редкий специфичный сценарий, в котором оба значения могут быть использованы. К примеру, значение по умолчанию может быть указано для того, чтобы код выполнился по определенному сценарию, в случае, если значение передано не было, но компонент все равно был отрисован, хоть и передал ошибку в консоли. Или если предусмотрено несколько сценариев поведения этого компонента, с default-значением и без.

Аналог из мира высокой кухни – нам вдруг резко захотелось ночью поесть чего-то сладкого, но в холодильнике только бутеры с майонезом и колбаской. Мы, конечно, их съедим и немного успокоимся. Но сладкого все равно хочется – будто загорается лампочка с сообщением об ошибке.

Сгенерировано нейросетью Kandinsky 2.2
Сгенерировано нейросетью Kandinsky 2.2

Финальный код

В итоге наш код со всеми валидациями будет выглядеть так:

 props: {
    // String
    pizzaName: {
      type: String,
      required: true,
      validator: (value) => value.length > 0,
    },

    // Number
     slicesCount: {
      type: Number,
      default: 4,
      validator: (value) => value >= 0,
    },

    // Boolean
    isHawaiian: {
      type: Boolean,
      default: false, // Помним почему здесь все равно указываем значение по умолчанию
    },

    // Array
    toppings: {
      type: Array,
      default: () => [],
      validator: (value) => value.length > 0,
    },

    // Object
    pizzaItem: {
      type: Object,
      default: () => ({}),
      validator: (value) => value.name,
    },
    
    // Даты
    delieveryDate: {
    type: Date, 
    validator: function(value) {
    const dateRegex = /^\d{2}-\d{2}$/;
    if (!value.match(dateRegex)) return false;
    const date = new Date(value);
    return date instanceof Date && !isNaN(date);
    }
    },

    // Function - анти-паттерн
    onClick: {
      type: Function,
      default: () => {},
    },

    // Также можно передавать Component
    component: {
      type: [Object, Function],
      default: () => {},
    },
    
// Можно даже передавать классы
pizzaClass: {
      type: PizzaClass,
      required: true,
      validator: (value) => value instanceof PizzaClass,
    },

    // Пример Props с любым типом, лишь бы не undefined
    definedProp: {
      required: true,
    },
};

Валидации в Composition API

Выглядят точно так же как и в Options API, что очень удобно и понятно, раз вы объявили props, то внутри просто настраиваете необходимые валидации аналогично тому, как вы делали это в Options API.

const props = defineProps({
  pizzaName: {
    type: String,
    required: true,
    validator: (value) => value.length > 0,
  },
});

Продвинутые кулинарные техники props

Уровень сложности: 4/5

Хуки во Vue

В какой момент создаются props? На этапе created-хука, и тут же указывается значение, если оно передается из родительского компонента. В противном случае подставится значение default value.

Важно отметить, что когда prop передается дочернему компоненту, значение prop по умолчанию перезаписывается переданным значением в перехватчике beforeCreate. Это означает, что к моменту вызова созданного перехватчика prop будет иметь переданное значение, а не значение по умолчанию. Поэтому, если у вас был вопрос, влияет ли установка дефолтного значения скорость отрисовки и происходит ли перерисовка компонента, то ответ – нет, не влияет, перерисовки и обновления тоже не происходит. 

Однако, если мы осуществляем какие-либо манипуляции со значениями, передаваемыми как props на поздних хуках (вроде mounted), то props также будут переписаны и компонент обновлен. 

Что такое принцип одностороннего потока данных (One way data flow)?

Props должны обрабатываться как данные, доступные только для чтения, и не должны изменяться непосредственно в дочернем компоненте. То есть технически возможно изменять props внутри дочернего компонента (например, когда мы передаем изменяемый объект (mutable object), но это строго не рекомендуется, так как может привести к непредвиденным последствиям, а также идет вразрез с принципом одностороннего потока данных.

Представим ситуацию. Мы заказываем в ресторане макароны по-флотски со спагетти и фаршем, обжаренным с кусочками красного перца и посыпанным укропом сверху. Мы передаем свой заказ и садимся в ожидании блюда. Но вместо того, чтобы готовить ингредиенты, как мы того ожидаем, повар ломает спагетти на четыре части, затем заменяет фарш на колбасу, а перец и укроп на лук. В итоге мы получаем совсем не то блюдо, которое хотели бы. 

Что произошло? Повар (дочерний компонент) изменил ингредиенты на свой лад (переписал props), хотя достаточно было спросить нас (родительский компонент) перед этим. Как бы повар спрашивает повара: что насчет изменить тип мяса на совершенно другой (this.$emit('change-meat-type', 'Колбаса')? Мы получаем этот запрос и реагируем на него: «Да, давай», меняем значение мяса с фарша на колбасу и в итоге получаем то, что мы ожидаем. Или обрабатываем полученный результат в голове  ("return acceptableMeats.includes('колбаса')") и возвращаем отказ.

Принцип одностороннего потока данных помогает гарантировать, что props в нашем приложении остаются предсказуемыми и их легко анализировать, что они используются предсказуемым и контролируемым образом. Сохраняя наш поток данных однонаправленным, мы можем избежать труднодоступных для отладки проблем, которые могут возникнуть из-за двусторонней привязки данных или других форм изменяемого состояния.

Когда дочернему компоненту необходимо взаимодействовать с родительским компонентом и передавать данные обратно вверх по иерархии компонентов, используется метод this.$emit(). Этот метод принимает два аргумента – имя события и данные, которые должны быть переданы родительскому компоненту. Например, если у вас есть компонент кнопки, который должен уведомлять родительский компонент при нажатии на него, вы могли бы определить событие следующим образом:

<button @click="$emit('change-meat-type', 'newMeat')">Заменить мясо</button>

В этом примере директива @click используется для прослушивания события click на кнопке. При нажатии кнопки вызывается метод this.$emit() с именем пользовательского события (при нажатии кнопки) и некоторыми данными, которые должны быть переданы родительскому компоненту ('некоторые данные').

Чтобы обработать генерируемое событие в родительском компоненте, вы можете использовать директиву “v-on” (или сокращенно @) для прослушивания события и вызова метода при его срабатывании. Например:

<template>
  <div>
    <meat-component :meat="meat" @change-meat-type="handleMyMeat($event)"></meat-component>
  </div>
</template>

<script>
import MeatComponent from './MeatComponent.vue';

export default {
  components: {
    MeatComponent,
  },
  data() {
    return {
        meat: "Фарш"
    }
  },
  methods: {
    handleMyMeat(value) {
        const acceptableMeats = ['Фарш', 'Индейка', 'Подозрительное мясо'];
        if (acceptableMeats.includes(value)) {
            this.meat = value
        };
        // Более простая запись - acceptableMeats.includes(value) ? this.meat = value : null;
    },
  },
};
</script>

В этом примере родительский компонент включает компонент MeatComponent и прослушивает событие нажатия кнопки, используя директиву @change-meat-type. Когда событие инициируется, вызывается метод handleMyMeat с данными, переданными из дочернего компонента.

Что такое props drilling? 

Сгенерировано нейросетью Kandinsky 2.2
Сгенерировано нейросетью Kandinsky 2.2

Представим, что вы шеф-повар на кухне, и каждому повару нужно посолить свое блюдо. Один повар берет солонку, солит блюдо соседнего повара, передает ему солонку, тот солит блюдо своего соседа. И так по цепочке до самого конца. А как насчет того, чтобы солонка стояла в центре кухни, чтобы каждый повар мог спокойно взять и посолить свое блюдо?

Проблема заключается в том, что передача props от родительского компонента к более глубоко вложенному становится сложнее и менее понятной, когда мы передаем свойства на большее количество уровней вложенности. Но чем дальше мы передаем props, тем больше у нас трудностей в отслеживании, понимании и отладке этих значений. Также прибавляются проблемы с производительностью, так как компонент обновляется каждый раз, когда меняется props, и, чем больше цепочка обновляемых компонентов, тем, представьте себе, хуже.

В одной из наших статей мы рассказывали о проблемах props drilling, но хоть она и относится к React-приложениям, эта проблема общая для обоих фреймворков.

Однако серьезность проблемы может варьироваться в зависимости от конкретного варианта использования, а также размера и сложности приложения. В целом props drilling может быть бо́льшей проблемой в React из-за отсутствия встроенного управления состоянием по сравнению с хранилищем Vuex в Vue.

Как альтернативу передаче данных через props в Vue можно использовать Vuex. Например, если у вас есть большой список блюд, поваров или объемная личная информация пользователя, то нет необходимости передавать ее каждый раз через props. Getters в Vuex кэшируются так же, как и computed значения в Vue, поэтому, если вы обращаетесь к одному и тому же getters-значению из разных компонентов, то вы не потеряете в производительности. Бонусом будет понятный код, отсутствие громоздких конструкций по передаче props и единое значение из getters для всех компонентов.

Также какое-то время использовался EventBus для взаимодействий, но в Vue 3 от него отказались, из-за проблемы схожей с миксинами – волшебный код, который сложно отслеживать со временем на больших проектах. Ему на замену пришла не менее волшебная, но более отслеживаемая связка Provide / Inject. Она есть и во Vue 2 c версии 2.2, но передаваемые данные не реактивны, поэтому данный способ не обрел популярности, вплоть до Vue 3, где передаваемые данные сделали реактивными. Но во второй версии можно обернуть provide значение в computed и получить какую-то реактивность.

Когда использовать props, а когда Vuex getters?

Props хорошо себя показывают в передаче данных на короткой дистанции, когда мы передаем данные на небольшую вложенность. Когда мы можем указать, что данное значение необходимо для корректного функционирования (required), или когда мы передаем данные, которые могут часто меняться и обновлять компонент. 

Getters хороши, когда нужно получить доступ к данным из централизованного хранилища. Getters схожи с computed значениями тем, что они кэшируются, то есть у многих компонентов в разных частях приложения будет доступ к закэшированному значению.

В итоге в контексте Vue 2 использование props для передачи данных должно быть в приоритете, getters же – для ситуаций, когда нам нужно передать данные на длинную дистанцию (помним, что изменение props также поведет за собой ререндеринг или перерисовку всех затронутых компонентов), или обеспечить доступ к общим данным из компонентов в разных частях приложения. 

Полезные правила eslint для props

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

1) vue/require-default-prop – подчеркивает необходимость указания либо default, либо required значения. Boolean значения игнорируются.

2) vue/require-prop-types  – подсвечивает ошибку, когда не были указаны типы props. Также не считается за ошибку проверка типа через кастомный валидатор.

3) vue/prop-name-casing  – определяет некорректное наименование props. Все значения должны быть наименованы в camelCase – это стандарты именования переменных, и многие библиотеки (как минимум все популярные и известные) им следуют.

4) vue/no-mutating-props – нельзя напрямую мутировать передаваемые значения, они предназначены только для чтения.

5) vue/require-prop-type-constructor – подсвечивает ошибку, когда вместо корректного типа (к примеру, Number) используется «Number».

Итоговый код будет такой:

{
  "extends": [
    "plugin:vue/recommended"
  ],
  "rules": {
    "vue/require-prop-type-constructor": "error",
    "vue/no-mutating-props": "error",
    "vue/prop-name-casing": ["error", "camelCase"],
    "vue/require-default-prop": "error",
    "vue/require-prop-types": "error"
  }
}

Заключение

Итак, мы подробно и на практических примерах рассмотрели практики реализации Vue props на проекте, как их лучше применять, а также как использовать валидацию.

Когда мы учимся чему-то новому, то часто сталкиваемся с абстрактными концепциями, методологиям и теориями, которые могут быть трудными для восприятия. Однако, когда мы видим простые и понятные примеры, таких как готовка, мы можем лучше понять, как это работает на практике и разобрать все по полочкам. Независимо от того, являетесь ли вы опытным Vue-разработчиком или только приближаетесь к пониманию того, как эффективно использовать props, осваивая лучшие практики, и анализируя, как делать не следует и как необходимо – все это является важнейшим багажом знаний и навыков, который принесет вам пользу во всех ваших текущих и будущих проектах.

Эта статья посвящена исключительно props, но если хочется ближе познакомиться с другими способами передачи данных и узнать об отличиях, то рекомендуем к прочтению этот материал. Здесь описывается взаимодействие компонентов внутри Vue 3, а именно – поведение props вместе с другими способами «общения» между компонентами.

Спасибо за внимание!

Авторские материалы для frontend-разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

Комментарии (5)


  1. TAZAQ
    05.10.2023 08:45

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

    Легально ли прокидывать класс как пропсу (в т.ч. через drilling) и уже где надо вызывать методы класса, а не эмитить?


    1. SimbirSoft_frontend Автор
      05.10.2023 08:45

      Разрешено все, что не запрещено (или не замечено :D).

      Чтобы понять, почему подход с прокидыванием экземпляра класса — плохой, давайте заменим его на нечто знакомое в мире Vue. Экземпляр класса — это ничто иное, как экземпляр родительского компонента, только вынесен в другой файлик. То есть у нас есть состояние, есть методы, которые это состояние меняют.

      Если обратиться к документации, где сказано, что мутирование props — это плохо, потому что приводит к боли, получается, что прокидывание самих методов с их последующим вызовом — это плохо и больно.

      Используя экземпляр класса в качестве props с последующим мутированием его состояния приведет к тому, что будет сложно отслеживать, кто же дернул этот метод. К тому же в классическом компонентном подходе, каждый компонент — это "черный ящик" с приватной реализацией собственной логики и состояния.

      Взаимодействие с компонентами осуществляется за счет публичного интерфейса — props/emits. В большинстве проектов и Vuex (помянем) / Pinia не нужны, так как хранить глобальное состояние отдельно от представления нужно далеко не всегда.

      Однако если уж прям хочется вынести логику в сервис, создав франкенштейна во фронтенде с костыльной слоистой архитектурой, то вам в Angular использование Vuex / Pinia будет наилучшим решением, чем придумывать слой данных на коленках. Даже в этом случае нужно будет обеспечивать высокую связанность компонентов и держать store/view слои где-то рядом (посмотрите на Gitlab исходники)

      Кратко: Нет, так делать не нужно, так как в долгой перспективе приведет к сильной связности/сложному дебагу и ужасному опыту тестирования


  1. domix32
    05.10.2023 08:45

    Или помните, как выше мы рассматривали динамическую и статическую передачу данных? Если мы передаем “:is-hawaiian="true"” – это “Boolean”, а если “is-hawaiian="true"”, то это уже значение “String”.

    то бишь двоеточие перед именем форсит валидацию типа? У вас столько кавычек в этом месте лишних, что не сразу поймёшь в чём разница.


    1. TAZAQ
      05.10.2023 08:45

      Когда появляется двоеточие, шаблонизатор воспринимает это как местную переменную, а когда двоеточия нет, это превращается в обычный html-атрибут, который имеет строковое значение


      1. SimbirSoft_frontend Автор
        05.10.2023 08:45

        Действительно, кавычки лишние мешали. Спасибо, что обратили внимание, поправили)