Урок 04

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: основы.