Provide и Inject
Проблема пропсов
Прокидывание через много уровней:
App → Layout → Sidebar → UserCard → UserAvatar
Каждый компонент должен принять и передать дальше. Prop drilling.
Решение — provide/inject
Родитель предоставляет данные (provide). Любой потомок может их взять (inject).
Без промежуточных компонентов.
Базовый пример
App.vue:
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);
</script>
<template>
<Layout />
</template>
UserAvatar.vue (глубоко в дереве):
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
</script>
<template>
<div :class="`theme-${theme}`">
Avatar
</div>
</template>
Промежуточные компоненты ничего не делают — просто рендерят.
Символьные ключи
Чтобы избежать коллизий:
// keys.js
export const ThemeKey = Symbol('theme');
export const UserKey = Symbol('user');
import { ThemeKey } from './keys';
provide(ThemeKey, theme);
const theme = inject(ThemeKey);
Правило: в больших проектах — символы.
Дефолтное значение
const theme = inject('theme', 'light');
Если провайдер отсутствует — вернётся 'light'.
Реактивность
Передавать можно ref или reactive:
provide('theme', ref('dark'));
В потомке:
const theme = inject('theme');
theme.value; // 'dark'
theme.value = 'light'; // ✅ меняет у родителя
provide функции
Часто дают метод изменения:
<script setup>
import { ref, provide } from 'vue';
const theme = ref('dark');
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
}
provide('theme', {
theme,
toggleTheme,
});
</script>
Потомок:
<script setup>
import { inject } from 'vue';
const { theme, toggleTheme } = inject('theme');
</script>
<template>
<p>Тема: {{ theme }}</p>
<button @click="toggleTheme">Переключить</button>
</template>
Полный пример — авторизация
App.vue:
<script setup>
import { ref, computed, provide } from 'vue';
const user = ref(null);
const isLoggedIn = computed(() => !!user.value);
function login(userData) {
user.value = userData;
}
function logout() {
user.value = null;
}
provide('auth', {
user,
isLoggedIn,
login,
logout,
});
</script>
<template>
<RouterView />
</template>
Header.vue:
<script setup>
import { inject } from 'vue';
const { user, isLoggedIn, logout } = inject('auth');
</script>
<template>
<header>
<span v-if="isLoggedIn">
Привет, {{ user.name }}
<button @click="logout">Выйти</button>
</span>
<RouterLink v-else to="/login">Войти</RouterLink>
</header>
</template>
Инъекция в setup vs вне
В setup:
const auth = inject('auth');
⚠️ Вне setup: вернёт undefined.
Если нужно в watch/mounted:
const auth = inject('auth');
watch(() => auth.user.value, () => {});
Типизация
TypeScript:
import type { Ref, ComputedRef } from 'vue';
type Auth = {
user: Ref<User | null>;
isLoggedIn: ComputedRef<boolean>;
login: (u: User) => void;
logout: () => void;
};
const { user } = inject<Auth>('auth')!;
provide readonly
Чтобы потомки не меняли:
import { provide, readonly, ref } from 'vue';
const count = ref(0);
provide('count', readonly(count));
Потомок не сможет записать. Методы передавать отдельно.
Где использовать
1. Тема. Тёмная/светлая для всего приложения.
2. Локаль. ru, en.
3. Авторизация. Юзер, методы логина.
4. Настройки. Глобальные конфиги.
5. Форма. Передача контекста формы в дочерние поля.
Form context — пример
Form.vue:
<script setup>
import { reactive, provide } from 'vue';
const form = reactive({
values: {},
errors: {},
touched: {},
});
function setValue(name, value) {
form.values[name] = value;
form.touched[name] = true;
}
function setError(name, error) {
form.errors[name] = error;
}
provide('form', { form, setValue, setError });
</script>
<template>
<form><slot /></form>
</template>
FormInput.vue:
<script setup>
import { inject, computed } from 'vue';
const props = defineProps({
name: String,
label: String,
});
const { form, setValue } = inject('form');
const value = computed({
get: () => form.values[props.name] ?? '',
set: (val) => setValue(props.name, val),
});
const error = computed(() => form.errors[props.name]);
</script>
<template>
<div>
<label>{{ label }}</label>
<input v-model="value">
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
FormInput не знает про form — получает через inject.
Частые ошибки
1. Строковый ключ совпадает в разных местах. Решение: символы.
2. Мутация readonly — ошибка. Решение: передавать методы.
3. Inject вне setup — undefined.
4. Забыть provide — вернётся дефолт или undefined.
Composable вместо provide
Альтернатива — модульный синглтон:
// composables/useAuth.js
import { ref, computed } from 'vue';
const user = ref(null);
export function useAuth() {
const isLoggedIn = computed(() => !!user.value);
const login = (u) => user.value = u;
const logout = () => user.value = null;
return { user, isLoggedIn, login, logout };
}
Использование:
import { useAuth } from '@/composables/useAuth';
const { user, login } = useAuth();
Плюс: проще, типизируется. Минус: глобальное состояние (не подходит для SSR без доп. настройки).
Итог
- provide/inject — передача данных через уровни
- Refs — для реактивности
- Символы — для ключей
- Функции через provide — для изменения
- readonly — защита от мутаций
Дальше — Vue Router: основы.