Vue Front-end Инженер

Vue Front-end Инженер

Роадмап навыков для прокачки

Паттерны использования ref и reactive в компонентах

VueJSComponentsComposition API

Основная идея

В компонентах Vue ref и reactive используются для разных паттернов управления состоянием. Выбор зависит от типа данных, структуры компонента и требований к переиспользованию логики.

Ключевые паттерны

  • ref для примитивов — счётчики, флаги, строки
  • ref для nullable — данные из API, выбранные элементы
  • reactive для форм — группа связанных полей
  • ref для composables — лучше возвращать из переиспользуемых функций
  • reactive для локального state — когда много связанных свойств

Типичные сценарии в компонентах

  • Одиночное значение (флаг, счётчик) → ref
  • Форма с несколькими полями → reactive или ref с объектом
  • Данные из API → ref (может быть null)
  • Composable → ref (проще возвращать и использовать)
  • Сложное вложенное состояние → reactive

Частые ошибки

  • Использование reactive для данных, которые могут быть заменены целиком
  • Деструктуризация reactive в composables (теряется реактивность)
  • Забывают .value при использовании ref в функциях
  • Смешивание ref и reactive без понимания разницы

Рекомендации

  • Начинайте с ref — он универсальнее
  • Используйте reactive для форм и локального состояния с несколькими полями
  • В composables возвращайте ref для лучшей совместимости
  • Будьте последовательны в проекте — выберите один основной подход

Введение и проблематика

При разработке компонентов важно выбрать правильный инструмент для управления состоянием. ref и reactive подходят для разных сценариев, и понимание этих паттернов помогает писать более чистый и предсказуемый код.

Официальная рекомендация Vue — использовать ref как основной инструмент. reactive подходит для специфичных случаев, когда удобнее работать без .value.


Паттерн 1: Простые значения

Для простых значений (флаги, счётчики, строки) используйте ref:

Code Example 1: Какой инструмент реактивности (ref или reactive) используется здесь для простых значений и почему это правильный выбор?

vue
<template>
  <div>
    <p>Загрузка: {{ isLoading ? 'Да' : 'Нет' }}</p>
    <p>Счётчик: {{ count }}</p>
    <p>Сообщение: {{ message }}</p>
  </div>
</template>
 
<script setup>
import { ref } from 'vue'
 
const isLoading = ref(false)
const count = ref(0)
const message = ref('')
 
function startLoading() {
  isLoading.value = true
}
 
function increment() {
  count.value++
}
</script>

Паттерн 2: Данные из API

Для данных, которые загружаются из API (и могут быть null), используйте ref:

Code Example 2: Почему для данных из API, которые могут быть null, используется ref, а не reactive?

vue
<template>
  <div v-if="user">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
  <p v-else>Загрузка...</p>
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
const user = ref(null)
 
onMounted(async () => {
  const response = await fetch('/api/user')
  user.value = await response.json()
})
</script>
⚠️

Нельзя использовать reactive для nullable значений — reactive не работает с null и примитивами.


Паттерн 3: Формы

Для форм с несколькими связанными полями удобно использовать reactive:

Code Example 3: Сравните два варианта реализации формы. Какие различия в работе с данными и сбросе формы между reactive и ref?

vue
<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.email" type="email" />
    <input v-model="form.password" type="password" />
    <label>
      <input v-model="form.rememberMe" type="checkbox" />
      Запомнить меня
    </label>
    <button type="submit">Войти</button>
  </form>
</template>
 
<script setup>
import { reactive } from 'vue'
 
// reactive удобен — нет .value для каждого поля
const form = reactive({
  email: '',
  password: '',
  rememberMe: false
})
 
function submitForm() {
  console.log(form.email, form.password)
}
 
function resetForm() {
  Object.assign(form, {
    email: '',
    password: '',
    rememberMe: false
  })
}
</script>

Паттерн 4: Composables

В composables рекомендуется возвращать ref — они лучше деструктурируются:

Code Example 4: Почему в composable возвращаются ref-значения? Что произойдёт при деструктуризации, если вернуть reactive?

js
// composables/useCounter.js
import { ref, computed } from 'vue'
 
export function useCounter(initial = 0) {
  const count = ref(initial)
  const doubled = computed(() => count.value * 2)
 
  function increment() {
    count.value++
  }
 
  function decrement() {
    count.value--
  }
 
  return {
    count,
    doubled,
    increment,
    decrement
  }
}

Использование:

vue
<script setup>
import { useCounter } from '@/composables/useCounter'
 
// Деструктуризация ref работает корректно
const { count, doubled, increment } = useCounter(10)
</script>
🚫

Если composable вернёт reactive, деструктуризация потеряет реактивность.


Паттерн 5: Локальное состояние компонента

Для компонентов с множеством связанных данных можно использовать reactive:

Code Example 5: Здесь используется reactive для хранения состояния компонента. В чём преимущество такого подхода и какие ограничения у него есть?

vue
<script setup>
import { reactive, computed } from 'vue'
 
const state = reactive({
  users: [],
  isLoading: false,
  error: null,
  currentPage: 1,
  searchQuery: ''
})
 
const filteredUsers = computed(() => {
  return state.users.filter(u =>
    u.name.includes(state.searchQuery)
  )
})
 
async function fetchUsers() {
  state.isLoading = true
  state.error = null
  try {
    const response = await fetch('/api/users')
    state.users = await response.json()
  } catch (e) {
    state.error = e.message
  } finally {
    state.isLoading = false
  }
}
</script>

Практические рекомендации

СценарийРекомендацияПричина
Одиночное значениеrefПростота, универсальность
Nullable данныеrefreactive не работает с null
Формаreactive или refОба подходят
ComposablerefБезопасная деструктуризация
Замена данных целикомrefreactive теряет реактивность
Много связанных полейreactiveМеньше .value

Пограничные кейсы

Передача в функции

Code Example 6: Почему для отслеживания свойства reactive-объекта используется геттер-функция, а для ref — передаётся напрямую?

vue
<script setup>
import { ref, reactive, watch } from 'vue'
 
const countRef = ref(0)
const stateReactive = reactive({ count: 0 })
 
watch(countRef, (newVal) => {
  console.log('ref изменился:', newVal)
})
 
watch(
  () => stateReactive.count,
  (newVal) => {
    console.log('reactive.count изменился:', newVal)
  }
)
</script>

Шаблонные refs

Для доступа к DOM-элементам и компонентам используйте ref:

Code Example 7: Почему для шаблонных refs (доступ к DOM-элементам и компонентам) всегда используется ref, а не reactive?

vue
<template>
  <input ref="inputRef" />
  <ChildComponent ref="childRef" />
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
const inputRef = ref(null)
const childRef = ref(null)
 
onMounted(() => {
  inputRef.value.focus()
  childRef.value.someMethod()
})
</script>

Плюсы и минусы в контексте компонентов

Аспектrefreactive
ФормыРаботаетУдобнее
API данныеИдеальноНе подходит для null
ComposablesРекомендуетсяПроблемы с деструктуризацией
Шаблонные refsТолько refНе применимо
Единообразие кодаМожно использовать вездеТолько для объектов

Вопросы интервьюера

Q: Когда в компонентах лучше использовать ref, а когда reactive?

ref — для примитивов, nullable данных, в composables. reactive — для форм и локального состояния с несколькими полями.

Q: Почему в composables рекомендуется возвращать ref?

ref безопасно деструктурируется. При деструктуризации reactive теряется реактивность.

Q: Можно ли использовать reactive для данных из API?

Нежелательно, если данные могут быть null или нужно заменить объект целиком. Лучше ref.

Q: Как сбросить форму на reactive?

Через Object.assign(form, initialValues) — нельзя переприсвоить переменную.


Источники

Code Example 1: Простые значения

❓ Какой инструмент реактивности (ref или reactive) используется здесь для простых значений и почему это правильный выбор?

vue
<template>
  <div>
    <p>Загрузка: {{ isLoading ? 'Да' : 'Нет' }}</p>
    <p>Счётчик: {{ count }}</p>
    <p>Сообщение: {{ message }}</p>
  </div>
</template>
 
<script setup>
import { ref } from 'vue'
 
const isLoading = ref(false)
const count = ref(0)
const message = ref('')
 
function startLoading() {
  isLoading.value = true
}
 
function increment() {
  count.value++
}
</script>

Code Example 2: Данные из API

❓ Почему для данных из API, которые могут быть null, используется ref, а не reactive?

vue
<template>
  <div v-if="user">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
  <p v-else>Загрузка...</p>
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
const user = ref(null)
 
onMounted(async () => {
  const response = await fetch('/api/user')
  user.value = await response.json()
})
</script>

Code Example 3: Форма — reactive vs ref

❓ Сравните два варианта реализации формы. Какие различия в работе с данными и сбросе формы между reactive и ref?

reactive:

vue
<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.email" type="email" />
    <input v-model="form.password" type="password" />
    <label>
      <input v-model="form.rememberMe" type="checkbox" />
      Запомнить меня
    </label>
    <button type="submit">Войти</button>
  </form>
</template>
 
<script setup>
import { reactive } from 'vue'
 
const form = reactive({
  email: '',
  password: '',
  rememberMe: false
})
 
function submitForm() {
  console.log(form.email, form.password)
}
 
function resetForm() {
  Object.assign(form, {
    email: '',
    password: '',
    rememberMe: false
  })
}
</script>

ref:

vue
<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.email" type="email" />
    <input v-model="form.password" type="password" />
    <label>
      <input v-model="form.rememberMe" type="checkbox" />
      Запомнить меня
    </label>
    <button type="submit">Войти</button>
  </form>
</template>
 
<script setup>
import { ref } from 'vue'
 
const form = ref({
  email: '',
  password: '',
  rememberMe: false
})
 
function submitForm() {
  console.log(form.value.email, form.value.password)
}
 
function resetForm() {
  form.value = {
    email: '',
    password: '',
    rememberMe: false
  }
}
</script>

Code Example 4: Composable

❓ Почему в composable возвращаются ref-значения? Что произойдёт при деструктуризации, если вернуть reactive?

js
// composables/useCounter.js
import { ref, computed } from 'vue'
 
export function useCounter(initial = 0) {
  const count = ref(initial)
  const doubled = computed(() => count.value * 2)
 
  function increment() {
    count.value++
  }
 
  function decrement() {
    count.value--
  }
 
  return {
    count,
    doubled,
    increment,
    decrement
  }
}
vue
<script setup>
import { useCounter } from '@/composables/useCounter'
 
const { count, doubled, increment } = useCounter(10)
</script>

Code Example 5: Локальное состояние компонента

❓ Здесь используется reactive для хранения состояния компонента. В чём преимущество такого подхода и какие ограничения у него есть?

vue
<script setup>
import { reactive, computed } from 'vue'
 
const state = reactive({
  users: [],
  isLoading: false,
  error: null,
  currentPage: 1,
  searchQuery: ''
})
 
const filteredUsers = computed(() => {
  return state.users.filter(u =>
    u.name.includes(state.searchQuery)
  )
})
 
async function fetchUsers() {
  state.isLoading = true
  state.error = null
  try {
    const response = await fetch('/api/users')
    state.users = await response.json()
  } catch (e) {
    state.error = e.message
  } finally {
    state.isLoading = false
  }
}
</script>

Code Example 6: Передача в watch — ref vs reactive

❓ Почему для отслеживания свойства reactive-объекта используется геттер-функция, а для ref — передаётся напрямую?

vue
<script setup>
import { ref, reactive, watch } from 'vue'
 
const countRef = ref(0)
const stateReactive = reactive({ count: 0 })
 
watch(countRef, (newVal) => {
  console.log('ref изменился:', newVal)
})
 
watch(
  () => stateReactive.count,
  (newVal) => {
    console.log('reactive.count изменился:', newVal)
  }
)
</script>

Code Example 7: Шаблонные refs

❓ Почему для шаблонных refs (доступ к DOM-элементам и компонентам) всегда используется ref, а не reactive?

vue
<template>
  <input ref="inputRef" />
  <ChildComponent ref="childRef" />
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
const inputRef = ref(null)
const childRef = ref(null)
 
onMounted(() => {
  inputRef.value.focus()
  childRef.value.someMethod()
})
</script>