5. Реактивность: ref и reactive
Реактивность Vue 3: ref и reactive ⚡
Заголовок раздела «Реактивность Vue 3: ref и reactive ⚡»Реактивность — это сердце Vue. Когда данные меняются, интерфейс обновляется автоматически. Не нужно вручную обновлять DOM — Vue делает это за тебя. Но чтобы Vue «видел» изменения, данные нужно обернуть в ref() или reactive(). Давай разберёмся, когда что использовать! 🎯
ref() — реактивная ссылка 📦
Заголовок раздела «ref() — реактивная ссылка 📦»ref() — универсальный способ сделать любое значение реактивным:
import { ref } from 'vue'
// Примитивыconst count = ref(0)const message = ref('Привет')const isVisible = ref(true)
// Объекты (тоже работает!)const user = ref({ name: 'Яша', age: 25 })
// Массивыconst items = ref<string[]>([])
// null (для async данных)const data = ref<User | null>(null).value — обязательный доступ в скрипте
Заголовок раздела «.value — обязательный доступ в скрипте»// В скрипте — всегда через .valueconsole.log(count.value) // 0count.value = 42 // изменяемcount.value++ // инкремент
// Объекты через .valueuser.value.name = 'Иван' // изменяем свойствоuser.value = { name: 'Пётр', age: 30 } // заменяем весь объект<!-- В шаблоне — .value НЕ нужен! Vue распаковывает автоматически --><template> <p>{{ count }}</p> <!-- не count.value! --> <p>{{ user.name }}</p> <!-- не user.value.name! --> <input v-model="message" /> <!-- не message.value! --></template>Почему .value в скрипте, но не в шаблоне?
В шаблоне Vue автоматически распаковывает (unwrap) ref. В скрипте — JavaScript не знает про Vue, поэтому нужен явный .value.
reactive() — реактивный объект 🗂️
Заголовок раздела «reactive() — реактивный объект 🗂️»reactive() делает объект (или массив) реактивным без .value:
import { reactive } from 'vue'
const user = reactive({ name: 'Яша', age: 25, address: { city: 'Москва', street: 'Пушкина' }})
// Работа как с обычным объектом!user.name = 'Иван' // ✅ реактивноuser.age++ // ✅ реактивноuser.address.city = 'СПб' // ✅ реактивно (глубокая реактивность!)<template> <p>{{ user.name }}, {{ user.age }} лет</p> <p>Город: {{ user.address.city }}</p></template>⚠️ Деструктуризация ломает реактивность!
Заголовок раздела «⚠️ Деструктуризация ломает реактивность!»const user = reactive({ name: 'Яша', age: 25 })
// ❌ ПЛОХО — деструктуризация теряет реактивность!const { name, age } = user// name и age — обычные примитивы, не реактивны// Изменение user.name НЕ обновит name
// ✅ ХОРОШО — toRefs сохраняет реактивностьimport { toRefs } from 'vue'const { name, age } = toRefs(user)// name.value и age.value — реактивны!// Изменение user.name ОБНОВИТ name.value ✅⚠️ reactive нельзя заменить целиком!
Заголовок раздела «⚠️ reactive нельзя заменить целиком!»let state = reactive({ count: 0 })
// ❌ Заменяем весь объект — реактивность потеряна!state = reactive({ count: 1 }) // переменная теперь новый объект // старые ссылки никуда не делись
// ✅ Правильно — обновляем свойстваstate.count = 1
// ✅ Или используй ref для замены объекта целикомconst state = ref({ count: 0 })state.value = { count: 1 } // ✅ работает!ref vs reactive — когда что? 🤔
Заголовок раздела «ref vs reactive — когда что? 🤔»✅ Используй ref() для:├── Примитивов (number, string, boolean)├── Когда нужно передавать реактивное значение в функции├── Когда нужно заменять объект целиком└── Composables (возвращать из функций)
✅ Используй reactive() для:├── Объектов с несколькими связанными свойствами├── Когда не нужна деструктуризация└── Состояния формы (group of related fields)
💡 Правило Эвана Ю (создатель Vue): Используй ref() везде, где не уверен — это безопаснее!// reactive — хорошо для формconst form = reactive({ email: '', password: '', rememberMe: false})
// ref — хорошо для отдельных значений и composablesconst count = ref(0)const isLoading = ref(false)const user = ref<User | null>(null)readonly() — неизменяемый реактивный объект 🔒
Заголовок раздела «readonly() — неизменяемый реактивный объект 🔒»import { ref, reactive, readonly } from 'vue'
const original = reactive({ count: 0 })const readonlyOriginal = readonly(original)
readonlyOriginal.count = 1 // ⚠️ Warning в dev, ничего не происходит
// Если original меняется, readonlyOriginal тоже обновляется// Но через readonlyOriginal изменить нельзя!original.count = 5console.log(readonlyOriginal.count) // 5 — синхронизируется!
// Типичное применение: expose readonly внутри composableconst useCounter = () => { const count = ref(0) const increment = () => count.value++
return { count: readonly(count), // нельзя изменить снаружи increment // можно только через increment }}shallowRef() и shallowReactive() — поверхностная реактивность 🏔️
Заголовок раздела «shallowRef() и shallowReactive() — поверхностная реактивность 🏔️»По умолчанию Vue делает глубокую реактивность — отслеживает изменения на всех уровнях вложенности. shallow варианты отслеживают только верхний уровень:
import { shallowRef, shallowReactive } from 'vue'
// shallowRef — реактивна только замена .value целикомconst bigList = shallowRef([{ id: 1, name: 'Яша' }])
// ❌ Не реактивно — вложенное изменениеbigList.value[0].name = 'Иван' // шаблон НЕ обновится
// ✅ Реактивно — замена .valuebigList.value = [...bigList.value, { id: 2, name: 'Иван' }]
// shallowReactive — реактивны только свойства верхнего уровняconst state = shallowReactive({ user: { name: 'Яша' }, // ← только эта ссылка отслеживается count: 0 // ← отслеживается})
state.count++ // ✅ реактивноstate.user = { name: 'Иван' } // ✅ реактивно (замена ссылки)state.user.name = 'Пётр' // ❌ НЕ реактивно (вложенное)Когда использовать shallow?
// Большие массивы с immutable данными (производительность!)const hugeList = shallowRef<HugeDataItem[]>([])
// Данные обновляются только заменой всего массиваconst refresh = async () => { hugeList.value = await fetchHugeList() // ← только это реактивно}toRef() и toRefs() — мосты к реактивности 🌉
Заголовок раздела «toRef() и toRefs() — мосты к реактивности 🌉»import { reactive, toRef, toRefs } from 'vue'
const user = reactive({ name: 'Яша', age: 25,})
// toRef() — создаёт ref на конкретное свойство reactiveconst nameRef = toRef(user, 'name')nameRef.value = 'Иван' // изменяет user.name!console.log(user.name) // 'Иван' — синхронизировано!
// toRefs() — создаёт объект с refs на все свойстваconst { name, age, email } = toRefs(user)// Теперь можно деструктурировать с сохранением реактивности!name.value = 'Пётр' // изменяет user.name!Главный use-case toRefs — в composables:
export function useUser() { const user = reactive({ name: 'Яша', role: 'admin', isActive: true })
// Возвращаем через toRefs — потребитель может деструктурировать! return { ...toRefs(user), // Методы возвращаем как есть updateUser: (data: Partial<User>) => Object.assign(user, data) }}
// В компонентеconst { name, role, isActive, updateUser } = useUser()// name, role, isActive — реактивные refs! ✅isRef(), isReactive(), unref() — утилиты 🔧
Заголовок раздела «isRef(), isReactive(), unref() — утилиты 🔧»import { ref, reactive, isRef, isReactive, unref } from 'vue'
const count = ref(0)const user = reactive({ name: 'Яша' })
// isRef() — проверка, является ли значение refisRef(count) // trueisRef(0) // falseisRef(user) // false
// isReactive() — проверка реактивного объектаisReactive(user) // trueisReactive(count) // false (ref, не reactive)
// unref() — безопасно распаковывает ref (или возвращает значение как есть)unref(count) // 0 (как count.value)unref(42) // 42 (обычное число, не ref)unref('hello') // 'hello'
// Полезно в composables, которые принимают ref ИЛИ значениеfunction useDouble(value: number | Ref<number>) { return computed(() => unref(value) * 2)}useDouble(5) // работает!useDouble(ref(5)) // тоже работает!Паттерны работы с реактивностью 💡
Заголовок раздела «Паттерны работы с реактивностью 💡»Паттерн 1: Инициализация async данных
Заголовок раздела «Паттерн 1: Инициализация async данных»import { ref, onMounted } from 'vue'
interface User { id: number; name: string }
const user = ref<User | null>(null)const loading = ref(false)const error = ref<string | null>(null)
const loadUser = async (id: number) => { loading.value = true error.value = null try { const response = await fetch(`/api/users/${id}`) user.value = await response.json() } catch (e) { error.value = 'Не удалось загрузить пользователя' } finally { loading.value = false }}
onMounted(() => loadUser(1))Паттерн 2: Состояние формы
Заголовок раздела «Паттерн 2: Состояние формы»import { reactive } from 'vue'
const form = reactive({ email: '', password: '', confirmPassword: '', acceptTerms: false})
const errors = reactive<Record<string, string>>({})
const validate = () => { if (!form.email) errors.email = 'Email обязателен' if (form.password.length < 8) errors.password = 'Минимум 8 символов' if (form.password !== form.confirmPassword) { errors.confirmPassword = 'Пароли не совпадают' }}
const resetForm = () => { Object.assign(form, { email: '', password: '', confirmPassword: '', acceptTerms: false }) Object.keys(errors).forEach(key => delete errors[key])}Паттерн 3: Список с операциями
Заголовок раздела «Паттерн 3: Список с операциями»import { ref, computed } from 'vue'
interface Todo { id: number text: string done: boolean}
const todos = ref<Todo[]>([])const filter = ref<'all' | 'active' | 'done'>('all')
const filteredTodos = computed(() => { if (filter.value === 'active') return todos.value.filter(t => !t.done) if (filter.value === 'done') return todos.value.filter(t => t.done) return todos.value})
const addTodo = (text: string) => { todos.value.push({ id: Date.now(), text, done: false })}
const toggleTodo = (id: number) => { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done}
const removeTodo = (id: number) => { todos.value = todos.value.filter(t => t.id !== id)}Ref unwrapping — автораспаковка 🎁
Заголовок раздела «Ref unwrapping — автораспаковка 🎁»Vue автоматически распаковывает refs в нескольких контекстах:
const count = ref(0)const state = reactive({ count }) // ref в reactive!
// Внутри reactive — .value не нужен!state.count // 0 (не state.count.value!)state.count = 5 // изменяет count.value!
// Но только для верхнего уровня reactiveconst nested = reactive({ inner: { count } })nested.inner.count.value // здесь .value нужен (не верхний уровень!)В шаблоне все refs распаковываются:
<template> <!-- Всё это работает без .value в шаблоне --> {{ count }} <!-- ref<number> --> {{ user.name }} <!-- ref<User> — Vue распакует user, потом .name --> {{ title }} <!-- ref<string> --></template>Резюме ⚡
Заголовок раздела «Резюме ⚡»Система реактивности Vue 3:
ref()— для примитивов и замены целых объектов, доступ через.valuereactive()— для объектов, доступ напрямую, но нельзя деструктурироватьreadonly()— неизменяемая обёртка, синхронизируется с источникомshallowRef/shallowReactive— для производительности с большими даннымиtoRef/toRefs— мосты между reactive и ref, для composablesisRef/unref— утилиты для работы в composables
Следующий шаг — computed и watch! 🔄