javascript

Vue 3: Почему ref() — это новая ссылка, а reactive() — обёртка?

  • суббота, 21 июня 2025 г. в 00:00:03
https://habr.com/ru/articles/920186/

🧩 Введение

Если вы работаете с Vue 3, вы точно сталкивались с ref() и reactive(). Обе функции из Composition API делают значения реактивными — но делают это по-разному. И хотя документация Vue чётко указывает, что использовать в каком случае, она редко объясняет, почему это важно и что может пойти не так, если использовать не тот инструмент.

Вот ссылки на официальную документацию — на всякий случай:

Вкратце:

  • ref() возвращает обёртку со свойством .value, в которую можно положить любое значение;

  • reactive() создаёт прокси-объект и отслеживает изменения напрямую в его полях.

На примерах из документации всё выглядит прозрачно. Но стоит использовать reactive() в ситуации, где ожидается независимая копия данных — и можно словить неожиданные баги. Например, форма может не сбрасываться, как ожидалось, а изменения "эталонных" данных начинают влиять на текущее состояние.

В одном из таких кейсов оказался и я. Всё выглядело логично, багов не было — до тех пор, пока не потребовалось сбрасывать фильтры. В результате — форма не очищалась, а объект-шаблон уже был "испорчен". Всё из-за того, что reactive() не копирует объект, а оборачивает его.

В этой статье я покажу:

  • в чём именно разница между ref() и reactive();

  • как легко попасть в ловушку ссылочной семантики;

  • и расскажу о реальном кейсе из проекта, который наглядно это демонстрирует.

⚙️ Краткая теория

Что такое ref()

ref() — это функция, которая оборачивает любое значение в объект с единственным свойством .value, и делает это значение реактивным.

Примеры:

const count = ref(0);
count.value++; // реактивно
const user = ref({ name: 'Alex' });
user.value.name = 'Bob'; // тоже реактивно

То есть, ref() создаёт контейнер, внутри которого лежит значение, и Vue следит за изменениями внутри него. Для примитивов — это вообще единственный способ сделать их реактивными.

Что такое reactive()

reactive() — это функция, которая принимает объект и возвращает его проксированную версию. Vue оборачивает каждое поле этого объекта реактивной обёрткой.

Пример:

const user = reactive({ name: 'Alex' });
user.name = 'Bob'; // реактивно
// но! примитивы работать не будут
const count = reactive(0); // ❌ не сработает, вернёт 0 как есть

reactive() работает только с объектами (включая массивы, Map и т.д.) и не добавляет никаких обёрток типа .value.

Главное отличие

Функция

Что делает

Где используется

ref(value)

Оборачивает значение в { value: value }

Примитивы, любые значения

reactive()

Создаёт прокси над объектом, следит за его полями

Только объекты

Типичный баг

Вот код, который кажется правильным:

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", а источник потенциальной боли. Особенно если спутать, где ты хочешь копию, а где работаешь по ссылке.

Ловите баги не в проде, а в голове. А если словили — пусть хотя бы с пользой.