Vue 3: Почему ref() — это новая ссылка, а reactive() — обёртка?
- суббота, 21 июня 2025 г. в 00:00:03
Если вы работаете с Vue 3, вы точно сталкивались с ref()
и reactive()
. Обе функции из Composition API делают значения реактивными — но делают это по-разному. И хотя документация Vue чётко указывает, что использовать в каком случае, она редко объясняет, почему это важно и что может пойти не так, если использовать не тот инструмент.
Вот ссылки на официальную документацию — на всякий случай:
Вкратце:
ref()
возвращает обёртку со свойством .value
, в которую можно положить любое значение;
reactive()
создаёт прокси-объект и отслеживает изменения напрямую в его полях.
На примерах из документации всё выглядит прозрачно. Но стоит использовать reactive()
в ситуации, где ожидается независимая копия данных — и можно словить неожиданные баги. Например, форма может не сбрасываться, как ожидалось, а изменения "эталонных" данных начинают влиять на текущее состояние.
В одном из таких кейсов оказался и я. Всё выглядело логично, багов не было — до тех пор, пока не потребовалось сбрасывать фильтры. В результате — форма не очищалась, а объект-шаблон уже был "испорчен". Всё из-за того, что
reactive()
не копирует объект, а оборачивает его.
В этой статье я покажу:
в чём именно разница между ref()
и reactive()
;
как легко попасть в ловушку ссылочной семантики;
и расскажу о реальном кейсе из проекта, который наглядно это демонстрирует.
ref()
— это функция, которая оборачивает любое значение в объект с единственным свойством .value
, и делает это значение реактивным.
Примеры:
const count = ref(0);
count.value++; // реактивно
const user = ref({ name: 'Alex' });
user.value.name = 'Bob'; // тоже реактивно
То есть, ref()
создаёт контейнер, внутри которого лежит значение, и Vue следит за изменениями внутри него. Для примитивов — это вообще единственный способ сделать их реактивными.
reactive()
— это функция, которая принимает объект и возвращает его проксированную версию. Vue оборачивает каждое поле этого объекта реактивной обёрткой.
Пример:
const user = reactive({ name: 'Alex' });
user.name = 'Bob'; // реактивно
// но! примитивы работать не будут
const count = reactive(0); // ❌ не сработает, вернёт 0 как есть
reactive()
работает только с объектами (включая массивы, Map и т.д.) и не добавляет никаких обёрток типа .value
.
Функция | Что делает | Где используется |
---|---|---|
| Оборачивает значение в | Примитивы, любые значения |
| Создаёт прокси над объектом, следит за его полями | Только объекты |
Вот код, который кажется правильным:
const original = { x: 1 };
const state = reactive(original);
state.x = 2;
console.log(original.x); // ⛔️ 2 — исходный объект тоже изменился
Почему? Потому что reactive()
не копирует объект, а оборачивает его напрямую. Это важнейший момент, и именно он часто становится источником багов — как в моём кейсе с фильтрами (о нём дальше).
В одном из проектов у нас были фильтры для споров — отдельный слайс состояния, который хранился в reactive
. Всё выглядело вполне стандартно.
Изначально фильтры создавались так:
export const INITIAL_DISPUTE_ADVANCED_FILTERS = prepareFieldsWithDefaults( DISPUTES_LIST_FILTER_FIELDS
);
Метод сброса фильтров выглядел тоже привычно:
const resetDisputesFilters = ({ disputesListFilters }: TDisputeRefs) => () => { Object.assign(disputesListFilters, INITIAL_DISPUTE_ADVANCED_FILTERS); };
А вот инициализация состояния:
const disputesListFilters = reactive( INITIAL_DISPUTE_ADVANCED_FILTERS
);
Выглядит нормально? На первый взгляд — да.
Но сброс фильтров почему-то перестал работать. Значения не сбрасывались корректно. Что пошло не так?
Я передал INITIAL_DISPUTE_ADVANCED_FILTERS
напрямую в reactive()
, и получил реактивную обёртку над оригиналом. То есть:
disputesListFilters
и INITIAL_DISPUTE_ADVANCED_FILTERS
— указатели на один и тот же объект;
любые изменения disputesListFilters
в рантайме портили шаблон;
а когда вызывался Object.assign(...)
— он просто "сбрасывал" текущие (уже изменённые) значения поверх самих себя.
По факту — сброс работал, просто сбрасывал на уже испорченную версию.
Решение оказалось простым: не оборачивать оригинал, а передавать копию.
const disputesListFilters = reactive({ ...INITIAL_DISPUTE_ADVANCED_FILTERS,
});
Теперь disputesListFilters
— отдельный объект, и сброс работает как ожидается: шаблон остаётся нетронутым, а состояние — управляемым.
reactive(obj)
— не копирует, а оборачивает переданный объект.
ref(obj)
— создаёт новую ссылку, к которой ты обращаешься через .value
.
Если работаешь с шаблоном или эталоном данных — делай копию вручную, иначе можно испортить оригинал незаметно.
Такие ошибки особенно коварны, потому что:
ни eslint
, ни tsc
на них не ругаются;
всё выглядит синтаксически правильно;
поведение "ломается" только в рантайме и не сразу.
В больших проектах отладка таких багов занимает много времени. Лучше один раз понять, как работает реактивность, чем потом часами искать, почему сброс формы ничего не сбрасывает.
На первый взгляд ref()
и reactive()
ведут себя предсказуемо — особенно если просто следовать документации. Но в реальных проектах всё не так просто.
Vue не делает глубокого копирования при работе с реактивностью. Это значит:
reactive()
не клонирует объект, а работает с ним напрямую через Proxy;
любые изменения происходят по ссылке, даже если вы об этом не подозреваете;
шаблонные объекты, переданные в reactive()
, могут мутировать прямо в рантайме — и вы об этом узнаете только когда начнут "ломаться" сбросы или сравнения.
Если не учитывать эту ссылочную семантику, можно получить баг, который проявляется не сразу, а спустя время — и выглядит как будто "всё сломалось само".
Именно поэтому важно понимать, что именно делает reactive()
, где он создаёт обёртку, а где вы продолжаете работать с оригиналом. И в каких ситуациях стоит делать явную копию, прежде чем оборачивать объект в реактивность.
При работе с реактивностью в Vue 3 важно понимать не только "что использовать", но и "как это работает".
ref()
подходит для примитивов, а также для ситуаций, где нужна явная ссылка на значение. Его поведение всегда однозначно: .value
— ваш друг.
reactive()
— мощный и удобный инструмент для объектов, особенно в формах и сложных структурах. Но не забывайте: он оборачивает объект, не копируя его.
Самая частая ошибка — передать в reactive()
эталонный объект, ожидая, что он останется нетронутым. Он не останется.
Перед тем как использовать реактивность, задай себе один простой вопрос:
“Мне нужна копия или ссылка?”
Если нужна копия — делай её сам. Vue за тебя этого не сделает.
🧠 В реальных проектах ref()
и reactive()
— не просто "синтаксис из Composition API", а источник потенциальной боли. Особенно если спутать, где ты хочешь копию, а где работаешь по ссылке.
Ловите баги не в проде, а в голове. А если словили — пусть хотя бы с пользой.