← Все статьи

Vue 3: главные ловушки реактивности и Composition API

ref vs reactive, потеря реактивности при деструктуризации, watchEffect и Teleport — разбор частых ошибок на Dev.to.

Содержание

Коротко

Vue 3 с Proxy, Composition API и встроенной поддержкой TypeScript выглядит проще Vue 2, но на практике «уметь пользоваться» и «использовать правильно» — разные уровни. На Dev.to собрали типичные ловушки реактивности, выбора между ref и reactive, наблюдателей и новых компонентов вроде Teleport и Suspense.

Что произошло

Реактивность переписали с Object.defineProperty на Proxy: исчезли классические боли Vue 2 — новые поля объекта и индексы массива без реакции. Зато появился новый ментальный налог: где хранится связь с обновлением DOM, если вы деструктурировали объект или подменили ссылку целиком.

Простое правило из статьи: ref — для примитивов и объектов, которые могут полностью замениться; reactive — для вложенных структур вроде форм и конфигов. Ловушка в том, что reactive возвращает прокси исходного объекта: при let { count } = state вы получаете обычное число, и инкремент не триггерит перерисовку. Выход — toRefs, чтобы каждое поле осталось ref-обёрткой.

Отдельная головная боль — автораспаковка ref. В шаблоне .value не нужен, но ref внутри обычного массива не распаковывается автоматически, а внутри reactive-массива — распаковывается. Ошибка «заменил весь объект reactive-переменной» лечится либо переходом на ref с присвоением data.value = …, либо Object.assign, сохраняющим ту же прокси-ссылку.

В Composition API автор сравнивает watch и watchEffect: второй сразу выполняется и сам собирает зависимости — удобно для логов, но опасно, если внутри менять те же ref (риск бесконечного цикла). Для контролируемых реакций на конкретное поле с «старым/новым» значением — watch.

Почему это важно

Vue 3 часто выбирают как «легче React», но половина багов в проде — не в API, а в разрыве реактивной цепочки: composable вернул .value вместо ref, lifecycle-хук зарегистрировали в setTimeout, scoped-стили не доходят до Teleport. Такие ошибки не ловятся линтером и проявляются только под нагрузкой или в SSR.

Для команд это означает: нужен явный контракт composable — «наружу только ref/reactive, не голые значения» — и ревью кода на деструктуризацию state. TypeScript в Vue 3 помогает, но только если props объявлять через defineProps<{…}>() и withDefaults, а не смешивать объявления на этапе выполнения и по типам без дисциплины.

На практике

  1. Деструктурируете reactive — оборачивайте в toRefs, иначе теряете реактивность.
  2. Данные могут полностью замениться — берите ref, не reactive.
  3. В watchEffect не мутируйте зависимости без явного условия выхода; для сравнения прежнего и нового значения — watch.
  4. Composable useXxx возвращает { x, y } как ref, не { x: x.value }.
  5. Хуки onMounted и др. вызывайте синхронно в setup, не из async-колбэка.
  6. Teleport переносит DOM, но контекст (props, inject) остаётся у родителя — для стилей нужен :deep() или глобальный CSS.
  7. Suspense и async-компоненты — осторожно в SSR и критичных маршрутах; пока экспериментальная зона.

Итог

Vue 3 не сложнее по синтаксису — сложнее по модели реактивности. Статья на Dev.to — хороший чеклист перед рефакторингом legacy на Composition API: ref/reactive, watch, composable-контракты и краевые случаи Teleport. Если ловили похожие баги — сверьтесь с оригиналом и примерами кода.