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- елемент рендериться завжди, а умова лише перемикає CSSdisplay: 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 3v-ifвиконується першим і не бачить змінної циклу. Фільтр - черезcomputedабо<template v-for>зv-ifусередині.
Щоб використати компонент у шаблоні, 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.