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

Vue: питання на співбесіді рівня Junior

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

36 питань

Обидва створюють реактивний стан, за яким 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.

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

Дані йдуть вниз через props, події - вгору через emits. Це односпрямований потік даних.

<!-- Counter.vue -->
<script setup>
const props = defineProps({ count: { type: Number, required: true } });
const emit = defineEmits(['update']);
</script>

<template>
  <button @click="emit('update', props.count + 1)">{{ props.count }}</button>
</template>
<!-- Батьківський компонент -->
<Counter :count="clicks" @update="clicks = $event" />

Головне правило: дочірній компонент не змінює props. Значення належить батькові: якщо дитина його змінить, батько при наступному рендері перезапише зміну, а Vue виведе попередження. Дитина просить зміну подією, а батько вирішує.

v-model на компоненті - скорочення для пари «prop + подія»:

<SearchInput v-model="query" />
<!-- те саме, що :modelValue="query" @update:modelValue="query = $event" -->

З Vue 3.4 у дочірньому компоненті це зручно оформлюється через defineModel().

Коли props і emits незручні: якщо дані треба передати через п'ять рівнів компонентів, які самі їх не використовують. Тоді - provide/inject або стор (Pinia).

Докладніше в документації: Props: односпрямований потік даних

  • v-if - умовний рендер: якщо умова хибна, елемента немає в DOM взагалі. При зміні умови елемент (чи компонент) створюється заново або знищується разом зі своїм станом, обробниками й дочірніми компонентами.
  • v-show - елемент рендериться завжди, а умова лише перемикає CSS display: none.
<Modal v-if="isOpen" />          <!-- немає в DOM, поки закрито -->
<div v-show="isExpanded">...</div> <!-- є завжди, лише прихований -->

Як обирати:

  • v-show - коли елемент перемикають часто (вкладки, розгортання блоків): перемкнути CSS дешевше, ніж щоразу створювати й знищувати.
  • v-if - коли умова змінюється рідко або елемент важкий і часто не потрібен узагалі: не витрачати час на початковий рендер того, що не покажуть.

Наслідки, про які питають:

  • З v-if стан дочірнього компонента (введений текст, прокрутка) скидається при кожному приховуванні. З v-show - зберігається.
  • v-if має v-else і v-else-if, v-show - ні. І v-show не працює на <template>.
  • Не ставте v-if і v-for на один елемент: у Vue 3 v-if виконується першим і не бачить змінної циклу. Фільтр - через computed або <template v-for> з v-if усередині.

Докладніше в документації: v-if проти v-show

Щоб використати компонент у шаблоні, Vue має знати, що це за тег. Є два способи його «зареєструвати».

Глобальна реєстрація - компонент доступний у шаблоні будь-якого компонента застосунку:

import { createApp } from 'vue';
import BaseButton from './components/BaseButton.vue';

const app = createApp(App);
app.component('BaseButton', BaseButton);
app.mount('#app');

Локальна реєстрація - компонент доступний лише там, де його імпортували. У <script setup> для цього досить імпорту:

<script setup>
import BaseButton from './BaseButton.vue';
</script>

<template>
  <BaseButton>Зберегти</BaseButton>
</template>

Чому документація Vue радить локальну реєстрацію:

  • tree shaking: глобально зареєстрований компонент потрапляє в збірку, навіть якщо ніде не використовується. Локальний - лише туди, де його імпортовано, і разом з тією частиною коду, що його потребує (важливо для розділення коду за сторінками);
  • явні залежності: з імпорту видно, звідки компонент. З глобальною реєстрацією в шаблоні з'являється тег, походження якого треба шукати - у великому проєкті це ускладнює підтримку й рефакторинг;
  • TypeScript і редактор краще розуміють імпортовані компоненти (підказки props, перехід до визначення).

Коли глобальна реєстрація доречна:

  • справді базові компоненти, що використовуються майже скрізь (кнопки, іконки) у невеликому застосунку;
  • компоненти бібліотеки UI, яку підключають як плагін (app.use(PrimeVue));
  • компоненти, які мають бути доступні в шаблонах, зібраних у браузері з HTML сторінки (див. Vue «острівцями» на Blade-сторінках).

Найменування:

  • у SFC - PascalCase: <BaseButton />. Так тег легко відрізнити від HTML-елементів;
  • шаблони в HTML-сторінці (не в .vue-файлах) нечутливі до регістру - там тільки kebab-case: <base-button></base-button>, і без самозакривних тегів.

Автоімпорт (плагін unplugin-vue-components) - компромісний варіант: у шаблоні компонент без імпорту, але в збірку він потрапляє як локальний.

Докладніше в документації: Реєстрація компонентів

Основні хуки Composition API:

Хук Коли спрацьовує
setup (сам <script setup>) створення компонента, до рендеру
onBeforeMount перед першим додаванням у DOM
onMounted компонент у DOM - можна працювати з елементами
onBeforeUpdate / onUpdated до й після оновлення DOM через зміну стану
onBeforeUnmount / onUnmounted перед і після видалення компонента
onActivated / onDeactivated для компонентів у <KeepAlive>
onErrorCaptured помилка в дочірньому компоненті
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

const width = ref(window.innerWidth);
const onResize = () => (width.value = window.innerWidth);

onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
</script>

Хуки мають викликатися синхронно в setup - не в setTimeout і не після await. Інакше Vue не знає, до якого компонента їх прив'язати.

Де завантажувати дані:

  • прямо в setup - запит починається якомога раніше, ще до монтування:
const users = ref([]);
fetch('/api/users').then((r) => r.json()).then((data) => (users.value = data));
  • onMounted - якщо для запиту потрібен DOM (розміри елемента) або код не повинен виконуватися при серверному рендері: onMounted на сервері не викликається;
  • watch з immediate: true або watchEffect - якщо дані залежать від props чи параметрів маршруту й мають перезавантажуватися при їх зміні. Найчастіший правильний варіант для сторінок.

Типові помилки:

  • доступ до DOM у setup чи onBeforeMount - елементів ще немає, template ref дорівнює null;
  • не прибрати за собою: обробники на window, таймери, підписки без onUnmounted - витоки пам'яті, особливо в SPA;
  • onUpdated для реакції на дані - для цього є watch; onUpdated спрацьовує на будь-яке оновлення компонента і легко зациклюється, якщо змінює стан;
  • гонитва запитів при швидкій зміні параметрів - потрібне скасування попереднього запиту.

Options API має відповідники (mounted, unmounted...), а також created - його роль у Composition API виконує сам setup.

Докладніше в документації: Хуки життєвого циклу

Атрибути, передані компоненту, але не оголошені як props чи emits, автоматично потрапляють на кореневий елемент компонента:

<!-- BaseButton.vue -->
<template>
  <button class="btn"><slot /></button>
</template>
<BaseButton class="large" id="save" data-test="save-button" @focus="onFocus">Зберегти</BaseButton>

Результат:

<button class="btn large" id="save" data-test="save-button">Зберегти</button>
  • class і style об'єднуються з тими, що вже є на кореневому елементі;
  • обробники подій (@focus) теж прокидаються - як слухачі на кореневому елементі;
  • оголошені props і emits не прокидаються - їх компонент обробляє сам.

inheritAttrs: false + v-bind="$attrs" - коли атрибути мають потрапити не на корінь, а на внутрішній елемент. Класичний приклад - поле введення з обгорткою:

<!-- BaseInput.vue -->
<script setup>
defineOptions({ inheritAttrs: false });
defineProps(['label']);
</script>

<template>
  <label class="field">
    {{ label }}
    <input v-bind="$attrs">
  </label>
</template>
<BaseInput label="Email" type="email" placeholder="you@example.com" required />

type, placeholder, required потрапляють на <input>, а не на <label>.

Компонент з кількома кореневими елементами не має «кореня» - атрибути нікуди не прокидаються автоматично, і Vue виводить попередження. Треба явно вказати v-bind="$attrs" на потрібному елементі.

Доступ у скрипті: useAttrs() у <script setup>. Значення $attrs не реактивне в сенсі watch - для реакції на зміну краще оголосити prop.

Пастки:

  • неоголошена подія прокидається як нативний слухач на корінь. Якщо компонент сам робить emit('click'), а click не оголошено в defineEmits, обробник батька спрацює двічі - від нативного кліку й від emit. Тому події варто оголошувати завжди;
  • class на компоненті з inheritAttrs: false теж не застосується до кореня автоматично - він входить у $attrs.

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

v-model - двостороння прив'язка поля форми до стану: введене потрапляє в змінну, зміна змінної - у поле.

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

const name = ref('');
const agree = ref(false);
const city = ref('kyiv');
const tags = ref([]);
</script>

<template>
  <input v-model="name">
  <input type="checkbox" v-model="agree">
  <select v-model="city">
    <option value="kyiv">Київ</option>
    <option value="lviv">Львів</option>
  </select>
  <label><input type="checkbox" value="php" v-model="tags"> PHP</label>
  <label><input type="checkbox" value="vue" v-model="tags"> Vue</label>
</template>

Для різних елементів v-model використовує різні властивості й події: value + input для тексту, checked + change для чекбоксів і радіо, value + change для select. Чекбокси з value, прив'язані до масиву, додають і прибирають значення.

Модифікатори:

  • .lazy - оновлювати на change (вихід з поля), а не на кожне натискання;
  • .number - перетворювати введене на число (parseFloat). Якщо перетворити неможливо, лишається рядок. Для type="number" застосовується автоматично;
  • .trim - обрізати пробіли на початку й у кінці.
<input v-model.trim="email" type="email">
<input v-model.number="age" type="number">
<textarea v-model.lazy="bio"></textarea>

Пастки:

  • v-model ігнорує атрибути value, checked, selected у розмітці - джерело істини лише стан. Початкове значення задається в ref;
  • select без початкового значення, що збігається з опцією, в iOS не дає вибрати першу опцію - тому додають порожню <option disabled value="">Оберіть місто</option>;
  • IME (введення ієрогліфів, частина мобільних клавіатур) - v-model не оновлює значення під час складання символу; якщо потрібно - власний обробник input;
  • v-model на prop - не можна: props належать батькові. Для компонента-поля - defineModel().

Значення не лише рядки: :value="{ id: 1 }" на опціях select чи чекбоксах дає змогу прив'язувати об'єкти, а true-value/false-value на чекбоксі - власні значення замість true/false.

Докладніше в документації: Прив'язка полів форми

У Vue 3 на одному елементі v-if має вищий пріоритет, ніж v-for. Умова обчислюється раніше за цикл - і не має доступу до змінної циклу.

<!-- помилка: todo ще не визначено, коли перевіряється v-if -->
<li v-for="todo in todos" v-if="!todo.done">{{ todo.title }}</li>

У Vue 2 було навпаки (v-for першим), тож код, перенесений зі старих проєктів, ламається саме тут.

Правильні варіанти:

1. Фільтр - в обчислюваній властивості (найкращий варіант для фільтрації):

<script setup>
const activeTodos = computed(() => todos.value.filter((todo) => !todo.done));
</script>

<template>
  <li v-for="todo in activeTodos" :key="todo.id">{{ todo.title }}</li>
</template>

Фільтр виконується лише при зміні todos, а не на кожен рендер, і шаблон стає простішим.

2. <template> з v-for зовні, v-if усередині - коли умова для кожного елемента окрема:

<template v-for="todo in todos" :key="todo.id">
  <li v-if="!todo.done">{{ todo.title }}</li>
</template>

3. v-if зовні - для умови на весь список:

<ul v-if="todos.length">
  <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
<p v-else>Завдань немає</p>

Що ще варто знати про v-for:

  • :key обов'язковий - стабільний ідентифікатор, а не індекс;
  • перебір об'єкта: v-for="(value, key, index) in object";
  • діапазон: v-for="n in 10" дає числа від 1 до 10;
  • на <template> key ставиться на сам <template>.

Чому computed краще за фільтр у шаблоні: вираз v-for="todo in todos.filter(...)" створює новий масив на кожному рендері компонента - навіть якщо змінилося щось зовсім не пов'язане зі списком.

Докладніше в документації: v-for разом з v-if

Для class і style Vue має розширений синтаксис :class / :style: крім рядків, вони приймають об'єкти й масиви.

Об'єкт - клас за умовою:

<button :class="{ 'opacity-50': loading, 'bg-red-600': danger }">Видалити</button>

Ключ - клас, значення - умова. Статичний class і динамічний :class об'єднуються:

<div class="card" :class="{ 'ring-2': selected }"></div>

Масив - кілька джерел класів:

<div :class="[sizeClass, { 'font-bold': active }, isError ? 'text-red-600' : 'text-gray-900']"></div>

Обчислювана властивість - коли логіка складніша:

const badgeClass = computed(() => ({
  paid: 'bg-green-100 text-green-800',
  pending: 'bg-amber-100 text-amber-800',
  failed: 'bg-red-100 text-red-800',
})[props.status]);

Стилі - об'єкт з camelCase чи kebab-case властивостями:

<div :style="{ width: progress + '%', backgroundColor: color }"></div>
<div :style="{ '--accent': theme.accent }"></div>   <!-- CSS-змінна -->

Vue автоматично додає вендорні префікси, якщо властивість їх потребує.

На компонентах: class і style, передані компоненту, додаються до класів його кореневого елемента.

Пастки з Tailwind:

  • не збирати назви класів з частин: :class="`bg-${color}-500`" - Tailwind сканує файли й генерує лише ті класи, які бачить повністю. Склеєних класів у CSS не буде. Потрібна мапа з повними назвами (як у прикладі з badgeClass);
  • конфлікти класів: class="p-4" на компоненті і p-2 всередині дають обидва класи - переможе той, що пізніше в згенерованому CSS, а не той, що «ззовні». Для керованого злиття - утиліти на кшталт tailwind-merge.

Динамічні значення з даних (ширина прогрес-бару, колір користувача) краще через :style чи CSS-змінні, а не через генерацію класів.

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

v-on (скорочено @) підписує обробник на подію DOM чи компонента:

<button @click="count++">+1</button>                 <!-- вираз -->
<button @click="save">Зберегти</button>              <!-- метод: отримає об'єкт події -->
<button @click="remove(item.id)">Видалити</button>   <!-- виклик з аргументами -->
<button @click="remove(item.id, $event)">...</button><!-- $event - сама подія -->

Модифікатори подій замість ручного event.preventDefault():

Модифікатор Що робить
.prevent preventDefault() - форма не перезавантажує сторінку
.stop stopPropagation()
.self лише якщо подія виникла на самому елементі
.once спрацювати один раз
.capture слухати у фазі перехоплення
.passive пасивний слухач (прокрутка без затримок)
<form @submit.prevent="submit">...</form>
<div class="overlay" @click.self="close">...</div>

Модифікатори клавіш і миші:

<input @keyup.enter="search">
<input @keydown.esc="clear">
<textarea @keydown.ctrl.enter="send"></textarea>
<button @click.right.prevent="openMenu">...</button>
<input @keyup.enter.exact="send">   <!-- лише Enter, без Shift/Ctrl -->

Доступні .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, системні .ctrl, .alt, .shift, .meta, а також будь-яка назва клавіші з event.key у kebab-case (.page-down).

Порядок модифікаторів має значення: @click.prevent.self скасовує дію для кліків на будь-якому дочірньому елементі, а @click.self.prevent - лише на самому елементі.

Події компонентів слухаються так само: @save="onSave". Модифікатори на кшталт .prevent до них не застосовуються - це не DOM-події.

Пастки:

  • .passive разом з .prevent - конфлікт: пасивний слухач не може скасувати дію, браузер проігнорує .prevent і виведе попередження;
  • важкі обробники на scroll, input, mousemove - потрібне обмеження частоти (debounce/throttle), Vue сам цього не робить;
  • обробники на window/document у шаблоні не повісиш - їх додають в onMounted і знімають в onUnmounted (або useEventListener з VueUse).

Докладніше в документації: Обробка подій

Vue Router зіставляє адресу в браузері з компонентом і показує його без перезавантаження сторінки.

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', name: 'user', component: UserProfile },
    { path: '/:pathMatch(.*)*', component: NotFound },   // усе інше - 404
  ],
});

app.use(router);
<template>
  <nav>
    <RouterLink to="/">Головна</RouterLink>
    <RouterLink :to="{ name: 'user', params: { id: 7 } }">Профіль</RouterLink>
  </nav>
  <RouterView />   <!-- тут рендериться компонент поточного маршруту -->
</template>

Динамічні параметри. :id у шляху - параметр. У компоненті він доступний через useRoute():

<script setup>
import { useRoute, useRouter } from 'vue-router';

const route = useRoute();    // поточний маршрут: params, query, hash, meta
const router = useRouter();  // навігація з коду

console.log(route.params.id);          // '7' - завжди рядок
router.push({ name: 'user', params: { id: 8 } });
</script>

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

  • параметри - рядки. route.params.id === 7 хибне - треба Number(route.params.id);
  • іменовані маршрути (name: 'user') надійніші за рядкові шляхи: при зміні URL посилання в коді не ламаються;
  • RouterLink замість <a href> - інакше браузер перезавантажить сторінку;
  • query-параметри (?page=2) - у route.query, вони не входять у шаблон шляху.

Vue Router 5 зберіг API четвертої версії й додав маршрутизацію на основі файлів (колишній unplugin-vue-router): маршрути генеруються зі структури каталогу src/pages, а route.params отримують точні типи в TypeScript.

Докладніше в документації: Vue Router: динамічні маршрути

Питання рівня Junior з реальних технічних співбесід - 36 питань у 9 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.

Інші рівні
Middle 37 Senior 35

Готуєтесь до співбесіди не просто так: зараз на сайті 7 відкритих вакансій рівня Junior. Переглянути вакансії