Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Реактивність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

Обидва створюють реактивний стан, за яким Vue стежить і перемальовує шаблон при змінах.

  • ref(value) - обгортка над будь-яким значенням: числом, рядком, об'єктом. Значення лежить у .value. У шаблоні .value писати не треба - Vue розгортає ref автоматично.
  • reactive(object) - реактивний проксі над об'єктом (чи масивом, Map). Поля читаються напряму, без .value.
import { ref, reactive } from 'vue';

const count = ref(0);
count.value++;

const form = reactive({ email: '', password: '' });
form.email = 'olia@example.com';

Обмеження reactive:

  • Працює лише з об'єктами - reactive(0) не має сенсу.
  • Не можна замінити цілком: form = reactive({...}) розриває зв'язок із шаблоном, бо шаблон стежить за старим проксі.
  • Деструктуризація губить реактивність: const { email } = form дає звичайний рядок.

Через ці обмеження документація Vue радить ref як основний інструмент: його можна передавати у функції, повертати з composables, перезаписувати цілком (items.value = newItems), і поведінка завжди однакова. reactive зручний для згрупованого стану на кшталт форми, який не замінюють і не деструктуризують.

Докладніше в документації: Основи реактивності

computed - значення, похідне від іншого стану. Vue кешує результат і перераховує його, лише коли змінилися реактивні залежності.

const items = ref([]);
const total = computed(() => items.value.reduce((sum, i) => sum + i.price, 0));

Метод у шаблоні ({{ calcTotal() }}) викликається при кожному рендері компонента, навіть якщо items не змінювалися. Для важких обчислень це зайва робота.

watch - не значення, а побічна дія у відповідь на зміну: запит на сервер, запис у localStorage, ручна робота з DOM.

watch(searchQuery, async (query) => {
  results.value = await search(query);
});

Як обирати:

  • Потрібно значення для шаблону чи коду - computed.
  • Потрібно щось зробити, коли значення змінилося, - watch.
  • Потрібна дія з аргументами за запитом (обробник кліку) - метод.

Типова помилка - watch, що просто записує похідне значення в інший ref:

watch(items, () => { total.value = sum(items.value); }, { deep: true }); // зайве

Це той самий computed, але з ручною синхронізацією, яку легко зламати.

computed має бути чистим: без запитів, змін іншого стану чи DOM. Інакше результат залежатиме від того, коли й скільки разів Vue вирішить його перерахувати.

Докладніше в документації: Обчислювані властивості

У шаблоні .value писати не треба - але лише для ref верхнього рівня, тобто змінних, оголошених прямо в <script setup> (чи повернених з setup()).

<script setup>
import { ref } from 'vue';

const count = ref(1);
const item = { id: ref(1) };   // ref усередині звичайного об'єкта
</script>

<template>
  <p>{{ count + 1 }}</p>      <!-- 2 -->
  <p>{{ item.id }}</p>        <!-- 1 -->
  <p>{{ item.id + 1 }}</p>    <!-- [object Object]1 -->
</template>

Чому так:

  • count - ref верхнього рівня, у виразі він розгорнутий;
  • item.id - ref усередині звичайного об'єкта. Вираз item.id + 1 додає до об'єкта-ref число;
  • {{ item.id }} без обчислень показує 1 - це окреме правило: якщо кінцевий результат інтерполяції - ref, текст розгортається. Але для будь-якого виразу з ним - ні.

Як виправити:

const { id } = item;           // зробити ref верхнього рівня

або явно {{ item.id.value + 1 }}.

Інші правила розгортання:

  • у reactive() ref, що лежить як властивість, розгортається при доступі: state.count, а не state.count.value;
  • у масивах і колекціях (reactive([ref(1)]), Map) - не розгортається, потрібно .value;
  • props приходять уже розгорнутими значеннями - .value не потрібен.

Практичне правило, що знімає плутанину: не класти ref-и всередину звичайних об'єктів. Або окремі ref-и на верхньому рівні, або один reactive/ref з об'єктом, поля якого - звичайні значення.

Інструменти допомагають: Vue - Official (Volar) у редакторі підсвічує, де .value потрібен, а TypeScript покаже, що item.id + 1 додає Ref<number> до числа.

Докладніше в документації: Особливість розгортання в шаблонах

За замовчуванням computed - лише для читання: значення виводиться з інших даних. Спроба записати дає попередження в консолі, і нічого не змінюється.

Записувана обчислювана властивість - об'єкт з get і set:

import { ref, computed } from 'vue';

const firstName = ref('Оля');
const lastName = ref('Коваль');

const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value) => {
    [firstName.value, lastName.value] = value.split(' ');
  },
});

fullName.value = 'Іра Лі';
firstName.value;   // 'Іра'

set не зберігає значення сам - він змінює вихідні дані, з яких get потім перерахує результат.

Типові застосування:

1. v-model над перетвореним значенням - наприклад, ціна в копійках у стані й у гривнях у полі:

const priceCents = ref(12550);

const priceUah = computed({
  get: () => (priceCents.value / 100).toFixed(2),
  set: (value) => { priceCents.value = Math.round(Number(value) * 100); },
});
<input v-model="priceUah" type="number" step="0.01">

2. Обгортка над props + emit для v-model на компоненті (до Vue 3.4 - стандартний шаблон, тепер його замінює defineModel):

const value = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v),
});

3. Синхронізація з URL чи сховищем: читання з параметра запиту, запис - через router.replace.

Що варто знати:

  • get має бути чистим: без побічних ефектів, запитів, змін іншого стану;
  • set не викликається, якщо змінити значення «всередині»: для об'єкта fullName.value.x = 1 змінює повернений об'єкт, а не викликає сеттер;
  • у Vue 3.4+ get отримує попереднє значення першим аргументом - зручно, щоб повернути той самий об'єкт, якщо нічого суттєво не змінилося, і не запускати зайві оновлення.

Докладніше в документації: Обчислювані властивості з записом

Vue оновлює шаблон лише тоді, коли змінюється реактивний стан - той, за яким система реактивності може стежити. Звичайна змінна такою не є.

<script setup>
let count = 0;                 // звичайна змінна
const increment = () => count++;
</script>

<template>
  <button @click="increment">{{ count }}</button>   <!-- завжди 0 -->
</template>

count справді збільшується в пам'яті, але Vue про це не знає й не перемальовує компонент.

Як правильно:

<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
</script>

Чому Vue не може стежити за звичайною змінною: JavaScript не дає способу перехопити присвоєння локальній змінній. Реактивність працює через перехоплення доступу до властивостей: ref - це об'єкт з властивістю value, reactive - проксі над об'єктом. Читання реєструє залежність, запис - сповіщає про зміну.

Коли звичайна змінна доречна:

  • константи й дані, які не змінюються (const MAX_ITEMS = 50);
  • функції-обробники;
  • значення, що потрібні лише в логіці й не показуються в шаблоні (ідентифікатор таймера, екземпляр сторонньої бібліотеки).

Схожі пастки:

  • заміна reactive-об'єкта цілком: let state = reactive({...}); state = reactive({...}) - шаблон стежить за старим проксі;
  • деструктуризація reactive чи props - отримані змінні не реактивні;
  • дані, отримані з fetch, записані в звичайну змінну замість ref - перший рендер порожній, і після завантаження нічого не змінюється.

Підказка: якщо значення має з'являтися в шаблоні й змінюватися - воно має бути ref, reactive чи computed.

Докладніше в документації: Оголошення реактивного стану