← Курс/toRef и toRefs: деструктуризация реактивных объектов#213 из 257+25 XP

toRef и toRefs: деструктуризация реактивных объектов

Проблема с деструктуризацией reactive()

Объект, созданный через reactive(), реактивен целиком. Но если вы деструктурируете его — извлечённые переменные **теряют реактивность**:

import { reactive } from 'vue'

const state = reactive({ count: 0, name: 'Алексей' })

// ПРОБЛЕМА: деструктуризация разрывает реактивную связь!
const { count, name } = state

count   // просто число 0 — не реактивное
name    // просто строка — не реактивное

state.count = 5
console.log(count) // всё ещё 0, не обновилось!

Почему это происходит? reactive() создаёт Proxy. При деструктуризации вы копируете текущее **примитивное значение**, а не ссылку на Proxy.

toRef(): реактивная ссылка на одно свойство

toRef(state, 'key') создаёт реактивный Ref, связанный с конкретным свойством объекта:

import { reactive, toRef } from 'vue'

const state = reactive({ count: 0, name: 'Алексей' })

const count = toRef(state, 'count')
const name = toRef(state, 'name')

// Теперь count — это Ref, связанный с state.count
state.count = 5
console.log(count.value) // 5 — реактивная связь сохранена!

count.value = 10
console.log(state.count) // 10 — изменение через ref отражается в state

Связь двусторонняя: изменение через ref обновляет исходный объект, и наоборот.

toRefs(): разбить весь объект на отдельные рефы

toRefs(state) принимает весь реактивный объект и возвращает объект, где каждое свойство — отдельный реактивный Ref:

import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: 'Алексей',
  city: 'Москва',
})

// Разбиваем на рефы — теперь можно безопасно деструктурировать
const { count, name, city } = toRefs(state)

state.count = 5
console.log(count.value) // 5 — всё работает!

name.value = 'Борис'
console.log(state.name) // 'Борис'

Применение в composables

Главный практический кейс toRefs — это возврат реактивных данных из composable-функций:

// useUser.js
import { reactive, toRefs } from 'vue'

export function useUser() {
  const state = reactive({
    name: '',
    email: '',
    isLoading: false,
  })

  async function fetchUser(id) {
    state.isLoading = true
    const data = await api.getUser(id)
    state.name = data.name
    state.email = data.email
    state.isLoading = false
  }

  // toRefs позволяет деструктурировать без потери реактивности
  return { ...toRefs(state), fetchUser }
}

// В компоненте:
const { name, email, isLoading, fetchUser } = useUser()
// name, email, isLoading — реактивные рефы

toRef vs toRefs: когда что использовать

| | toRef | toRefs |

|---|---|---|

| Синтаксис | toRef(state, 'key') | toRefs(state) |

| Результат | Один Ref | Объект из Ref-ов |

| Когда использовать | Нужна ссылка на одно свойство | Нужно деструктурировать весь объект |

| Применение | Пропсы, отдельные поля | Composables, spread-возврат |

toRef с пропсами

toRef удобен для создания writeable-ссылки на проп:

// В компоненте
const props = defineProps({ modelValue: Number })
const emit = defineEmits(['update:modelValue'])

// Создаём ref, который читает из props и пишет через emit
const localValue = toRef(props, 'modelValue')

Примеры

Демонстрация потери реактивности при деструктуризации и реализация toRef/toRefs через Proxy

// Демонстрируем проблему деструктуризации и реализуем toRef/toRefs

// Упрощённая реализация reactive через Proxy
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) { return target[key] },
    set(target, key, value) {
      target[key] = value
      console.log(`  [reactive] ${String(key)} = ${value}`)
      return true
    }
  })
}

// Реализация toRef — создаёт объект с .value, связанный с source[key]
function toRef(source, key) {
  return {
    get value() {
      return source[key]
    },
    set value(newVal) {
      source[key] = newVal  // пишем напрямую в source (это вызовет Proxy setter)
    }
  }
}

// Реализация toRefs — создаёт объект из toRef для каждого ключа
function toRefs(source) {
  const refs = {}
  for (const key in source) {
    refs[key] = toRef(source, key)
  }
  return refs
}

// === Демонстрация проблемы ===
console.log('=== ПРОБЛЕМА: деструктуризация reactive ===')
const state = reactive({ count: 0, name: 'Алексей', city: 'Москва' })

// Деструктуризация — просто копируем примитивные значения
const { count, name } = state
console.log('Начальные значения: count =', count, ', name =', name)

state.count = 42
console.log('После state.count = 42:')
console.log('  state.count =', state.count)  // 42
console.log('  count =', count)               // 0 (!!!) — потеря реактивности

// === Решение: toRef ===
console.log('\n=== РЕШЕНИЕ: toRef ===')
const state2 = reactive({ score: 0, username: 'Борис' })

const score = toRef(state2, 'score')
const username = toRef(state2, 'username')

console.log('score.value =', score.value)         // 0

state2.score = 100
console.log('После state2.score = 100:')
console.log('  score.value =', score.value)       // 100 — реактивная связь!

score.value = 200
console.log('После score.value = 200:')
console.log('  state2.score =', state2.score)     // 200 — двусторонняя связь!

// === Решение: toRefs ===
console.log('\n=== РЕШЕНИЕ: toRefs ===')
const state3 = reactive({ x: 10, y: 20, label: 'Точка A' })

const { x, y, label } = toRefs(state3)
console.log('x.value =', x.value, ', y.value =', y.value)

state3.x = 50
state3.y = 75
console.log('После изменения state3:')
console.log('  x.value =', x.value)   // 50
console.log('  y.value =', y.value)   // 75

label.value = 'Точка B'
console.log('После label.value = "Точка B":')
console.log('  state3.label =', state3.label)  // 'Точка B'

// === Эмуляция composable ===
console.log('\n=== Паттерн composable с toRefs ===')
function useCounter(initial = 0) {
  const state = reactive({ count: initial, step: 1 })

  function increment() { state.count += state.step }
  function setStep(n) { state.step = n }

  return { ...toRefs(state), increment, setStep }
}

const { count: counter, step, increment, setStep } = useCounter(5)
console.log('counter.value =', counter.value)  // 5
increment()
console.log('После increment(): counter.value =', counter.value)  // 6
setStep(10)
increment()
console.log('После setStep(10) + increment(): counter.value =', counter.value)  // 16