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

Junior: питання на співбесіді з теми «Компоненти й Composition API»

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

5 питань

Дані йдуть вниз через 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.

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