Skip to Content
VueКонтракт компонента

Контракт компонента

Уровень: middle.

Вопрос

Как компонент принимает значение снаружи, отдаёт его потомкам и куда деваются атрибуты, которые не объявлены как пропсы?

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

Во Vue 3 v-model на компоненте — это проп modelValue и событие update:modelValue. Именованная модель пишется как v-model:title и даёт пару title / update:title. С Vue 3.4 макрос defineModel() возвращает ref: запись в него сама эмитит обновление.

provide / inject передают значение любому потомку без проброса пропсов через промежуточные узлы. Это зависимость для составного виджета (Tabs и Tab, форма и поле), а не замена Pinia. Чтобы связь осталась реактивной, отдают ref или reactive. Чтобы потомок не мутировал состояние молча, отдают readonly и функцию-мутатор.

Атрибуты, не объявленные в props и emits (class, style, id, слушатели), Vue вешает на корневой элемент. inheritAttrs: false отключает это, и их ставят вручную через v-bind="$attrs". Несколько корней без явной привязки дают предупреждение: Vue не знает, куда их положить.

Кастомная директива — только низкоуровневая работа с DOM. Если задачу закрывает компонент, директива не нужна.

Разбор

<script setup> const model = defineModel() const title = defineModel('title') </script>

До defineModel то же самое писали руками: defineProps, defineEmits и emit('update:modelValue', value).

Ключ для provide лучше делать через InjectionKey, а не строку. inject('user') теряет тип. Symbol, связанный с InjectionKey<Ref<UserContext>>, проверяет и сторону provide, и сторону inject.

Сквозные атрибуты на обёртке кнопки:

<script setup> defineOptions({ inheritAttrs: false }) </script> <template> <div class="wrap"> <button v-bind="$attrs"><slot /></button> </div> </template>

Корень здесь div, а disabled и @click должны попасть на button.

Хуки директивы повторяют жизненный цикл элемента: mounted, updated, unmounted. Практические случаи: v-click-outside, маска ввода, v-permission, которая убирает узел, если роли нет. Проверку роли в шаблоне через v-if обычно проще читать. Директива оправдана, когда одно и то же DOM-поведение нужно повесить на разные теги без обёртки.

На что смотрят на senior

Provide не заменяет стор приложения. Контекст таба умирает вместе с <Tabs>. Корзина и сессия живут дольше дерева и не должны требовать, чтобы потребитель сидел внутри конкретного родителя.

emits, которые не объявили, утекут в $attrs как нативные слушатели. На фрагменте это особенно заметно: клик «пропадает» или садится не на тот узел.

Директива, которая читает стор и удаляет узел, плохо тестируется и обходит реактивность: роль сменилась, а элемент уже снят с DOM. Для прав доступа чаще оставляют v-if или не рендерят маршрут.

Типичные ошибки

  • Объявлять модель Vue 3 как value и input.
  • Класть в provide сырой объект и ждать, что потомок увидит замену поля.
  • Забывать inheritAttrs: false на обёртке и получать class и на корне, и на внутреннем теге, если повесили $attrs дважды.
  • Писать директиву ради логики, которой место в composable.

Задача

Tabs должен знать активную вкладку, Tab на три уровня ниже не должен получать её пропом через промежуточные обёртки. Куда класть состояние и как не дать потомку записать его напрямую?

Показать ответ

provide в Tabs: readonly(active) и функция select. Потомок делает inject. Для типов ключ — InjectionKey, не строка.

Pinia здесь лишняя: вкладка живёт вместе с виджетом, не со всем приложением. Без readonly любой Tab сможет заменить ref молча, и источник правды размажется.

Last updated on