v-model на компоненте и provide/inject

Делаем собственный компонент совместимым с v-model и узнаём, как прокидывать данные глубоко в дерево без цепочки пропсов — через provide/inject.

defineModel позволяет вашему компоненту работать с v-model, как нативный input. provide/inject передаёт данные от предка любому потомку напрямую, минуя промежуточные компоненты.

v-model на своём компоненте

В разделе 2 v-model работал на <input>. Свой компонент тоже может его поддерживать. В Vue 3.4+ для этого есть макрос defineModel — он создаёт реактивную привязку, синхронизированную с родителем:

<!-- MyInput.vue -->
<template>
  <input :value="model" @input="model = $event.target.value">
</template>

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

Теперь родитель использует компонент с v-model, как обычное поле:

<!-- App.vue -->
<template>
  <MyInput v-model="name" />
  <p>Вы ввели: {{ name }}</p>
</template>

<script setup>
import { ref } from 'vue'
import MyInput from './components/MyInput.vue'
const name = ref('')
</script>

model — это специальный ref: меняешь его внутри компонента — обновляется name у родителя, и наоборот. Двусторонняя связь без ручного emit.

Проблема «бурения пропсов»

Иногда данные (например, тема оформления или текущий пользователь) нужны глубоко вложенному компоненту. Передавать их пропсами через каждый промежуточный уровень утомительно — это называют «props drilling» (бурение пропсов). Промежуточные компоненты вынуждены тащить чужой проп.

provide / inject

provide в предке «публикует» значение, а inject в любом потомке его получает — сколько бы уровней между ними ни было:

<!-- App.vue (предок) -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>

<!-- DeepButton.vue (потомок на любой глубине) -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')   // 'dark', промежуточные уровни не нужны
</script>

Модель «провайд по ключу» на JS

Идея проста: предок кладёт значение в общее хранилище по ключу, потомок достаёт по тому же ключу. Промоделируем:

const context = new Map();

function provide(key, value) { context.set(key, value); }
function inject(key) { return context.get(key); }

// Предок публикует
provide("theme", "dark");
provide("user", "Аня");

// Потомок на любой глубине достаёт — без передачи через промежуточные уровни
console.log("Тема:", inject("theme"));
console.log("Пользователь:", inject("user"));
console.log("Неизвестный ключ:", inject("lang"));

Вывод:

Тема: dark
Пользователь: Аня
Неизвестный ключ: undefined

Когда что применять

ЗадачаИнструмент
Передать данные на 1 уровень внизпропсы
Двусторонняя привязка к компонентуv-model + defineModel
Прокинуть данные глубоко без цепочки пропсовprovide / inject
Глобальное состояние всего приложенияPinia (раздел 6)

Итог

  • defineModel делает компонент совместимым с v-model для двусторонней связи.
  • «Props drilling» — передача пропа через много уровней лишь чтобы донести его вглубь.
  • provide публикует значение в предке, inject получает его в любом потомке.
  • Для глобального состояния приложения лучше Pinia, а не provide/inject.
Проверьте себя
1. Что делает макрос defineModel в компоненте?
AСоздаёт глобальное хранилище
BОбъявляет пропсы
CДелает компонент совместимым с v-model, создавая двустороннюю привязку с родителем
DРегистрирует событие click
2. Какую проблему решают provide и inject?
AЗамедление рендеринга
BВалидацию форм
CПодключение внешних библиотек
DПередачу данных глубоко вложенному потомку без «бурения пропсов» через каждый промежуточный уровень
3. Что обычно лучше использовать для глобального состояния всего приложения?
AХранилище Pinia
Bprovide/inject на корне
CГлобальные переменные window
DlocalStorage напрямую