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Делает компонент совместимым с v-model, создавая двустороннюю привязку с родителем
CОбъявляет пропсы
DРегистрирует событие click
2. Какую проблему решают provide и inject?
AЗамедление рендеринга
BПередачу данных глубоко вложенному потомку без «бурения пропсов» через каждый промежуточный уровень
CВалидацию форм
DПодключение внешних библиотек
3. Что обычно лучше использовать для глобального состояния всего приложения?
Aprovide/inject на корне
BГлобальные переменные window
CХранилище Pinia
DlocalStorage напрямую
Поддержать проект