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

от автора

🧩 Введение

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/920186/