Олег Марков
Ref и Reactive в Vue 3 - понятное и подробное объяснение
Введение
Ref и Reactive в Vue 3 — это основа реактивности в Composition API. От того, как вы их понимаете и используете, сильно зависит предсказуемость и удобство работы с состоянием в приложении.
Здесь мы разберем, что такое ref и reactive, чем они отличаются, как они устроены "под капотом", в каких ситуациях лучше применять каждый из них, и какие типичные ошибки допускают разработчики. Смотрите, я буду идти от простых примеров к более тонким нюансам, чтобы вам было проще выстроить цельную картину.
Что такое реактивность в Vue 3
Кратко о механизме реактивности
Реактивность в Vue — это автоматическое отслеживание зависимостей между данными и шаблоном или вычислениями. Как только вы меняете значение реактивного состояния, Vue понимает, какие компоненты или вычисления зависят от этого состояния, и автоматически их обновляет.
В Vue 3 реактивность реализована с помощью Proxy и специальных оберток:
- ref — для примитивов и иногда для объектов
- reactive — для объектов, массивов и более сложных структур
Под капотом Vue:
- Перехватывает чтение и запись свойств;
- При чтении "подписывает" текущий эффект (компонент, computed, watcher) на это свойство;
- При записи уведомляет подписанные эффекты, и они перерасчитываются.
Ref — реактивная обертка над значением
Что такое ref
Ref — это объект, у которого есть одно ключевое свойство value. Внутри этого свойства хранится ваше значение, а сам объект является реактивным.
Общий вид:
import { ref } from 'vue'
const count = ref(0) // count — это объект
console.log(count.value) // 0
count.value++ // изменение значения
Важно понимать:
- count — не число, это объект;
- текущее значение числа лежит в count.value;
- Vue отслеживает чтение и запись свойства value.
Когда использовать ref
Ref имеет смысл использовать в трех основных случаях:
- Примитивы: число, строка, boolean, null, undefined
- Значение, которое вы хотите передать и изменять как "одно поле", даже если это объект
- Ссылки на DOM-элементы или компоненты (через template ref)
Смотрите, сейчас разберем пример, который часто встречается в коде.
import { ref } from 'vue'
export default {
setup() {
const message = ref('Привет') // реактивная строка
const counter = ref(0) // реактивное число
// метод, который изменяет значение
const increase = () => {
counter.value++ // увеличиваем число
}
return {
message,
counter,
increase,
}
},
}
В шаблоне вы можете использовать их так:
<template>
<p>{{ message }}</p> <!-- Vue сам разворачивает .value -->
<p>{{ counter }}</p>
<button @click="increase">+</button>
</template>
В шаблоне Vue автоматически "разворачивает" ref, поэтому .value писать не нужно. В JavaScript-коде .value обязателен.
Поведение ref в шаблонах и в коде
Здесь важно не перепутать:
- В шаблоне: вы работаете с ref как с обычным значением
- В JS/TS-коде: вам нужно явно обращаться к .value
Посмотрите этот пример:
const count = ref(0)
// Неправильно - потеряете реактивность
let plainNumber = count // plainNumber — это ref, а не число
// Правильно - сохраняете число, но оно не реактивно
let plainNumber2 = count.value
// Если хотите изменить ref - всегда через .value
count.value = 10
Обратите внимание, если вы сохраняете сам ref в другую переменную, вы сохраняете реактивную обертку, а не значение. Это может быть полезно при передаче ref в другие функции.
Ref с объектами
Ref можно использовать и для объектов:
const user = ref({
name: 'Иван',
age: 30,
})
В этом случае:
- user — это ref;
- user.value — это объект;
- изменения внутри user.value также отслеживаются реактивно.
user.value.age++ // это изменение будет реактивным
Но такой подход обычно используют реже, чем reactive, и чаще в тех случаях, когда вам нужно менять "весь объект целиком", например, замещать его новым.
user.value = { name: 'Анна', age: 25 } // меняем весь объект
Reactive — реактивный объект
Что такое reactive
Reactive создает проксируемый объект, в котором все поля отслеживаются реактивно. В отличие от ref, здесь не нужно .value.
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Привет',
})
Теперь:
- state.count — реактивное свойство;
- state.message — тоже реактивное свойство;
- вы работаете "как с обычным объектом", но он обернут в Proxy.
Пример использования:
const increase = () => {
state.count++ // Vue отследит это изменение
}
const changeMessage = () => {
state.message = 'Новое сообщение'
}
В шаблоне такой объект используется напрямую:
<template>
<p>{{ state.count }}</p>
<p>{{ state.message }}</p>
<button @click="increase">+</button>
</template>
Когда использовать reactive
Обычно reactive выбирают, когда:
- Вам нужно сгруппировать несколько связанных полей состояния
- Вы хотите работать с объектами и массивами "как обычно"
- Вам нужно иметь "единый объект состояния" вроде store внутри компонента
Например:
const form = reactive({
email: '',
password: '',
rememberMe: false,
})
Теперь вы можете обновлять поля формы:
const resetForm = () => {
form.email = ''
form.password = ''
form.rememberMe = false
}
И использовать их в шаблоне:
<template>
<input v-model="form.email" />
<input v-model="form.password" type="password" />
<input v-model="form.rememberMe" type="checkbox" />
</template>
Ключевые отличия ref и reactive
Отличие 1 — форма данных
- ref — всегда объект-обертка с полем value;
- reactive — Proxy-объект, который выглядит как обычный объект или массив.
Здесь я покажу вам разницу через типичный TypeScript-код:
const count = ref(0)
// Тип: Ref<number>
const state = reactive({
count: 0,
})
// Тип: { count: number } (на самом деле Proxy, но вид как у объекта)
Отличие 2 — использование примитивов
Ref создан специально для примитивов. Reactive с примитивами использовать нельзя:
// Так не работает как ожидается
// const num = reactive(0) // Vue выдаст предупреждение
const num = ref(0) // Правильный вариант
Reactive подходит только для:
- объектов
- массивов
- Map / Set и других коллекций (в зависимости от настроек)
Отличие 3 — поведение при деструктуризации
Это один из самых важных и часто "болезненных" моментов.
Посмотрите, что происходит при деструктуризации reactive:
const state = reactive({
count: 0,
message: 'Привет',
})
const { count, message } = state
Теперь:
- count — это обычное число, не реактивное;
- message — обычная строка.
Если вы будете менять state.count, значения count и message уже не обновятся. Реактивность утрачена, потому что вы "оторвали" примитив от объекта.
С ref картина другая:
const count = ref(0)
const other = count
other.value++ // изменится и count.value
Здесь ref остается одним и тем же объектом, вы просто копируете ссылку на него.
Для reactive деструктуризацию нужно делать через специальные утилиты, о которых мы поговорим ниже.
Работа с ref и reactive в Composition API
Возврат значений из setup
В setup вы обычно возвращаете нужные значения, чтобы использовать их в шаблоне. Смотрите пример:
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const state = reactive({
message: 'Привет',
items: [],
})
const addItem = () => {
state.items.push(`Элемент ${state.items.length + 1}`)
}
return {
count,
state,
addItem,
}
},
}
Здесь:
- в шаблон попадут count, state и addItem;
- Vue автоматически "развернет" ref, поэтому в шаблоне вы увидите его без .value;
- reactive-объект в шаблоне используется напрямую.
Почему ref разворачивается в шаблоне
Vue делает это специально, чтобы вам не приходилось постоянно писать .value в разметке. Это правило действует только в шаблонах.
В JavaScript-коде внутри setup, computed, watch и других функций вы всегда работаете с .value, если это ref с примитивом.
Частые практические сценарии использования
Сценарий 1 — простой счетчик с ref
Давайте разберемся на базовом примере:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0) // создаем реактивное число
const increment = () => {
count.value++ // изменяем значение
}
const reset = () => {
count.value = 0
}
return {
count,
increment,
reset,
}
},
}
В шаблоне:
<template>
<p>Значение - {{ count }}</p>
<button @click="increment">+</button>
<button @click="reset">Сбросить</button>
</template>
Vue будет отслеживать изменения count.value и обновлять разметку.
Сценарий 2 — форма с reactive
Теперь вы увидите, как это выглядит в коде со сложным объектом:
import { reactive } from 'vue'
export default {
setup() {
const form = reactive({
email: '',
password: '',
agree: false,
})
const submit = () => {
// Здесь form.email, form.password и form.agree уже содержат актуальные данные
// Можно отправить их на сервер
console.log('Отправка формы', form)
}
const reset = () => {
// Сбрасываем поля формы
form.email = ''
form.password = ''
form.agree = false
}
return {
form,
submit,
reset,
}
},
}
В шаблоне:
<template>
<form @submit.prevent="submit">
<input v-model="form.email" placeholder="Email" />
<input v-model="form.password" type="password" placeholder="Пароль" />
<label>
<input v-model="form.agree" type="checkbox" />
Согласен с условиями
</label>
<button type="submit">Отправить</button>
<button type="button" @click="reset">Сбросить</button>
</form>
</template>
Reactive здесь особенно удобен, потому что поля формы логически связаны и удобно живут внутри одного объекта.
Как связаны ref и reactive
Вложенные объекты внутри ref
Когда вы создаете ref с объектом, Vue автоматически делает вложенный объект реактивным, используя тот же механизм, что и для reactive. То есть:
const user = ref({
name: 'Иван',
age: 30,
})
Внутри user.value у вас получается реактивный объект. Это значит:
user.value.name = 'Анна' // это изменение будет реактивным
Vue "глубоко" оборачивает объект в Proxy, чтобы отслеживать все вложенные свойства.
Вложенные структуры внутри reactive
Reactive всегда возвращает проксируемый объект. Если внутри него есть вложенные объекты или массивы, они тоже становятся реактивными при первом доступе.
const state = reactive({
user: {
name: 'Иван',
},
tags: ['vue', 'reactivity'],
})
Теперь:
state.user.name = 'Анна' // реактивно
state.tags.push('typescript') // тоже реактивно
Vue "лениво" превращает вложенные структуры в реактивные по мере использования.
Преобразование между ref и reactive
Иногда нужно сделать так, чтобы свойства reactive-объекта были ref, например, чтобы удобно деструктурировать. Для этого есть утилиты:
- toRefs
- toRef
Они не создают копий значений, а делают "связанные" ref, которые указывают на оригинальные свойства.
Проблема деструктуризации reactive и ее решение
Что происходит при обычной деструктуризации
Посмотрите на пример, который часто приводит к ошибкам:
const state = reactive({
count: 0,
message: 'Привет',
})
const { count, message } = state
Здесь:
- count — обычное число;
- message — обычная строка;
- реактивность для этих переменных потеряна.
Если вы измените state.count, переменная count не обновится.
Использование toRefs
Чтобы сохранить реактивность при деструктуризации reactive-объекта, используются toRefs:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Привет',
})
const { count, message } = toRefs(state)
// Теперь count и message — это ref, связанные с state.count и state.message
Теперь:
count.value++ // увеличит state.count
state.count++ // изменит count.value
message.value = 'Новое' // изменит state.message
Это удобно, когда вы хотите:
- деструктурировать объект, чтобы не писать state.count везде;
- передавать отдельные поля как ref в другие функции или компоненты.
Использование toRef для одного поля
Если вам нужно создать ref только для одного свойства, можно использовать toRef:
import { reactive, toRef } from 'vue'
const state = reactive({
count: 0,
message: 'Привет',
})
const count = toRef(state, 'count')
// count — это ref, связанный с state.count
count.value++ // изменит state.count
Это полезно, когда вы хотите работать только с одним полем объекта, но не хотите оборачивать весь объект в toRefs.
Computed и watch с ref и reactive
Работа с computed
Computed-зависимости одинаково хорошо работают и с ref, и с reactive.
С ref:
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => {
// Здесь мы читаем count.value
return count.value * 2
})
С reactive:
import { reactive, computed } from 'vue'
const state = reactive({
count: 0,
})
const double = computed(() => {
// Здесь мы читаем state.count
return state.count * 2
})
Vue сам отслеживает, какие реактивные значения вы читаете внутри computed-функции, и пере вычисляет вычисление, когда они меняются.
Работа с watch
Watch наблюдает за реактивными значениями и выполняет колбэк при изменении.
С ref:
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
// Здесь вы увидите, как меняется count.value
console.log('count изменился', oldValue, '->', newValue)
})
Обратите внимание, здесь вы передаете сам ref в watch, а не count.value.
С reactive-объектом:
import { reactive, watch } from 'vue'
const state = reactive({
count: 0,
})
// Следим за конкретным полем через функцию
watch(
() => state.count,
(newValue, oldValue) => {
console.log('state.count изменился', oldValue, '->', newValue)
}
)
Если вы передадите весь объект в watch, нужно помнить про параметр deep.
watch(
state,
(newVal, oldVal) => {
// Этот watch будет реагировать на изменения любых свойств state
},
{ deep: true } // глубинное слежение
)
Template refs — ссылки на DOM и компоненты
Что такое template ref
Ref в Vue используется не только для данных, но и для ссылок на DOM-элементы и дочерние компоненты.
Пример:
import { ref, onMounted } from 'vue'
export default {
setup() {
const inputRef = ref(null) // сюда попадет DOM-элемент input
onMounted(() => {
// Здесь inputRef.value уже указывает на реальный DOM-элемент
// Например, можно поставить фокус
if (inputRef.value) {
inputRef.value.focus() // фокусируем input
}
})
return {
inputRef,
}
},
}
В шаблоне:
<template>
<input ref="inputRef" />
</template>
Здесь:
- inputRef — ref, который в момент монтирования компонента начнет указывать на DOM-элемент;
- inputRef.value — это сам элемент
<input>.
Template ref на компоненты
Вы можете получить ссылку и на дочерний компонент:
<!-- Parent.vue -->
<template>
<ChildComponent ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)
onMounted(() => {
// childRef.value — это экземпляр компонента ChildComponent
// Можно вызвать у него публичный метод, если он его предоставляет
if (childRef.value) {
// Например, childRef.value.focusInput()
}
})
</script>
Здесь я показываю вам пример, когда дочерний компонент предоставляет методы, к которым родитель может обратиться через ref.
Типичные ошибки при использовании ref и reactive
Ошибка 1 — забыли .value в коде
Очень частая ситуация:
const count = ref(0)
const inc = () => {
// Ошибка - так вы меняете не значение, а сам ref
// count = count + 1
// Правильно
count.value = count.value + 1
}
Комментарии:
- ref — это объект;
- чтобы изменить значение, нужно менять поле value;
- переприсваивать сам count нельзя, иначе вы потеряете связь с реактивной оберткой.
Ошибка 2 — деструктуризация reactive без toRefs
Вы уже видели эту ситуацию, но она настолько частая, что стоит отдельно напомнить:
const state = reactive({
count: 0,
})
// Неправильно — потеряете реактивность
const { count } = state
Здесь count — обычное число. Любые изменения state.count не обновят переменную count.
Правильно:
import { toRefs } from 'vue'
const { count } = toRefs(state) // count — это ref
Теперь Vue сохраняет связь между полем объекта и ref.
Ошибка 3 — reactive с примитивом
Иногда пытаются сделать:
// Неправильно - reactive не предназначен для примитивов
// const num = reactive(0)
// Правильно
const num = ref(0)
Reactive нужен для объектов и массивов. Для отдельных значений — только ref.
Ошибка 4 — мутация не реактивного объекта
Ситуация, когда вы создаете объект, а в реактивном состоянии храните только ссылку на него:
const original = { count: 0 }
// ref хранит ссылку на объект
const state = ref(original)
// Мутация оригинала
original.count++
// Значение state.value.count тоже изменится, но
// Vue может не отследить это изменение, если оно не прошло через реактивную обертку
Чтобы Vue точно отследил изменение, лучше всегда менять данные через референс, который создан реактивными утилитами:
state.value.count++ // Vue отследит изменение
Рекомендации по выбору между ref и reactive
Когда лучше использовать ref
Используйте ref, когда:
- У вас есть один примитив (число, строка, boolean и т.д.)
- Вам нужно передать или вернуть отдельно одно значение
- Вы работаете с template ref (DOM или компоненты)
- Вам нужен "указатель" на значение, которое иногда заменяется целиком
Примеры:
- счетчик;
- флаг загрузки;
- текущий выбранный id;
- ссылка на DOM-элемент.
Когда лучше использовать reactive
Используйте reactive, когда:
- Вам нужен объект состояния с несколькими полями
- Вы хотите логически сгруппировать данные
- У вас есть сложные структуры — вложенные объекты, массивы
Примеры:
- форма с несколькими полями;
- объект настроек;
- состояние модуля (user, settings, filters и т.д.).
Смешанное использование
Часто удобно комбинировать оба подхода:
const loading = ref(false)
const user = reactive({
id: null,
name: '',
email: '',
})
Здесь:
- loading — отдельный флаг;
- user — сгруппированное состояние по пользователю.
Такой подход делает структуру кода понятной и предсказуемой.
Заключение
Ref и Reactive в Vue 3 решают одну задачу — предоставить реактивное состояние, но делают это разными способами и для разных сценариев. Ref удобен для отдельных значений и ссылок на DOM или компоненты. Reactive удобен для объектов и массивов, когда важно работать с набором связанных полей "как с обычным объектом".
Ключевые моменты, которые важно удерживать в голове:
- ref — это объект с полем value
- reactive — это Proxy-объект, без поля value
- в шаблонах ref разворачивается автоматически, в коде — нет
- деструктуризация reactive без toRefs ломает реактивность
- reactive не используют для примитивов
Если вы будете четко разделять случаи использования ref и reactive, код станет проще, а проблем с "необновляющимся" интерфейсом будет значительно меньше. При работе с более сложными сценариями (комплексные объекты, перенос состояния между модулями) особенно помогает понимание того, что ref — это всегда "контейнер", а reactive — "живая копия" объекта через Proxy.
Частозадаваемые технические вопросы по теме статьи и ответы на них
Как правильно передать reactive-состояние в дочерний компонент и при этом сохранить типизацию
Обычно в пропсы передают либо весь объект reactive, либо отдельные поля. Чтобы типизация была корректной:
- Описывайте тип состояния отдельно:
interface UserState {
name: string
age: number
}
- Создавайте состояние через reactive
:
const user = reactive<UserState>({ name: '', age: 0 })
- В дочернем компоненте описывайте пропс как UserState, а не как ReturnType
. Тогда TS будет работать с "формой" объекта, а не с Proxy-типа.
Как сделать глубокий watch за объектом, созданным через reactive, но не отслеживать лишние изменения
Если вам нужно глубинное слежение только за частью объекта:
- Создайте вычисляемую функцию, которая возвращает только нужный фрагмент:
watch(
() => ({ filter: state.filter, sort: state.sort }),
(newVal) => {
// Реагируем только на изменения filter и sort
},
{ deep: true }
)
- Так вы не будете срабатывать на изменения других свойств state.
Как связать ref с localStorage или sessionStorage
Для двусторонней связи:
- Создайте ref и инициализируйте значением из localStorage:
const theme = ref(localStorage.getItem('theme') || 'light')
- Поставьте watch на ref и обновляйте хранилище:
watch(
theme,
(value) => {
localStorage.setItem('theme', value)
},
{ immediate: true }
)
Так ref всегда будет синхронизирован с localStorage.
Почему при передаче reactive-объекта в библиотечный код иногда "ломается" реактивность
Некоторые библиотеки клонируют объект (через JSON.parse(JSON.stringify) или другие техники). При этом Proxy Vue теряется. Чтобы избежать этого:
- Передавайте минимум данных, не весь объект state;
- Если библиотека мутирует объект, лучше передавать "чистые" данные (через structuredClone или JSON-методы), а затем вручную синхронизировать изменения с реактивным состоянием.
Как хранить в reactive нестандартные типы (например Date, Map, Set) и не потерять поведение
Vue может работать с Date, Map, Set, но есть нюансы:
- Для Date обычно достаточно ref
или поле Date в reactive-объекте. Изменяйте значение через новые объекты: date.value = new Date(). - Для Map и Set используйте методы, а затем "подталкивайте" Vue к обновлению, если нужно:
const state = reactive({
ids: new Set<number>(),
})
const addId = (id: number) => {
state.ids.add(id)
}
Vue отслеживает сами вызовы add и delete, но если есть проблемы с обновлением, можно дополнительно хранить счетчик или массив-отображение и обновлять его вместе с коллекцией.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев