Vue
Утилиты реактивности: toRef, toRefs, unref
Композабл возвращает reactive-объект с полями x и y координат мыши. В компоненте разработчик пишет const { x, y } = useMouse() и удивляется: значения замёрзли на первоначальных. Деструктуризация reactive-объекта разрывает связь с Proxy, и x, y становятся обычными числами-снимками. Решение - toRefs: он превращает каждое поле reactive-объекта в отдельный ref, и деструктуризация сохраняет реактивность. Этот же приём лежит в основе storeToRefs из Pinia.
- Композаблы вроде useMouse или useWindowSize из VueUse возвращают набор реактивных значений, которые хочется деструктурировать
- Деструктуризация props внутри setup без потери реактивности отдельного поля
- storeToRefs в Pinia: вытащить state и getters стора в локальные ref
- Утилитарные функции, принимающие 'ref или просто значение', где unref снимает обёртку единообразно
- Передача одного поля reactive-объекта в другой композабл как самостоятельного ref через toRef
Предварительные знания
- Различие ref (значение в .value) и reactive (Proxy на объект)
- Понимание, что деструктуризация копирует значения, а не ссылки на свойства
- Базовое знакомство с props в script setup
Почему деструктуризация ломает реактивность
Реактивность reactive-объекта держится на Proxy: перехват идёт при обращении к свойству через сам объект. Деструктуризация const { x } = state читает значение свойства один раз и кладёт в новую переменную. С этого момента x - обычное число, никак не связанное с Proxy. Изменение state.x обновит объект, но локальная x останется прежней.
Та же ловушка с props. Деструктуризация const { count } = props внутри setup даёт снимок значения на момент деструктуризации, а не реактивную ссылку на проп (за исключением реактивной деструктуризации props в более новых сборках, но базовый механизм тот же). Чтобы сохранить связь, поле нужно превратить обратно в ref.
Ошибка коварна тем, что код выглядит рабочим: значения корректны при первом рендере. Проблема всплывает позже, когда источник меняется, а деструктурированная переменная остаётся замороженной.
Почему const { x } = reactiveState теряет реактивность?
toRef и toRefs
toRefs проходит по всем свойствам reactive-объекта и для каждого создаёт ref, привязанный к исходному свойству. Чтение .value такого ref идёт в объект, запись - обратно в него. После toRefs деструктуризация безопасна: каждая переменная остаётся реактивным ref.
toRef работает точечно: создаёт один ref на конкретное свойство. Это нужно, когда из объекта берётся только одно поле или когда поле передаётся в другой композабл как самостоятельный ref. В Vue 3.3 toRef умеет принимать функцию-геттер, превращая любое вычисляемое выражение в ref только для чтения.
| Утилита | Что делает | Когда применять |
|---|---|---|
| toRefs | Все свойства объекта -> объект из ref | Деструктуризация всего reactive-объекта или возврата композабла |
| toRef(obj, key) | Одно свойство -> ref, связанный с ним | Нужен один реактивный ref из поля объекта |
| toRef(getter) | Геттер -> readonly ref | Превратить вычисление в ref для передачи дальше |
Композабл хранит состояние в reactive-объекте, а в компоненте его деструктурируют. Что вернуть из композабла?
unref, isRef и аналогия со storeToRefs
unref - короткая форма проверки: если аргумент это ref, вернуть его .value, иначе вернуть сам аргумент. Это удобно в утилитарных функциях, которые должны принимать и ref, и обычное значение, не заставляя вызывающего разворачивать вручную. isRef отвечает на вопрос строго: является ли значение ref, и используется для ветвления логики.
storeToRefs из Pinia - прямое применение этой идеи к стору. Сам стор это reactive-объект, поэтому деструктуризация его state и getters так же теряет реактивность. storeToRefs оборачивает state и getters в ref (но не методы-actions, которые и так привязаны), позволяя деструктурировать стор безопасно.
Логика та же, что у toRefs: реактивные данные нужно превращать в ref перед деструктуризацией. Pinia лишь добавляет правило не трогать actions - функции и так стабильны, оборачивать их в ref не имеет смысла.
Зачем нужен unref в утилитарной функции?
Связь с другими темами
Урок завершает набор утилит реактивности и готовит к Pinia:
- ref и reactive — Все утилиты этого урока - мост между двумя примитивами реактивности
- shallowRef и toRaw — toRaw из соседнего урока относится к тому же семейству утилит вокруг реактивных объектов
- Pinia — storeToRefs - прямое применение идеи toRefs к стору состояния
Итог
- Деструктуризация reactive-объекта разрывает реактивность: поля становятся обычными значениями-снимками
- toRefs превращает каждое свойство reactive-объекта в отдельный ref, так что деструктуризация сохраняет связь с источником
- toRef создаёт один ref, привязанный к конкретному свойству reactive-объекта или к источнику в виде функции-геттера
- unref возвращает значение из ref или само значение, если это не ref - удобно для функций, принимающих и то и другое
- isRef проверяет, является ли значение ref; storeToRefs в Pinia применяет toRefs к стору для безопасной деструктуризации
Связанные уроки
- vue-06-ref-reactive — Утилиты строятся вокруг ref и reactive, поэтому нужно твёрдо понимать оба примитива
- vue-19-shallow-raw — toRaw из соседнего урока - часть того же набора утилит вокруг реактивных объектов
- vue-27-pinia-intro — storeToRefs в Pinia делает ровно то же, что toRefs: позволяет деструктурировать стор без потери реактивности