Питання на співбесіді з Vue
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
108 питань
Компонент повідомляє батька про події через emit. Оголошувати події не обов'язково технічно, але дуже бажано.
<script setup>
const emit = defineEmits(['save', 'cancel']);
function onSubmit() {
emit('save', { title: title.value });
}
</script>
<PostForm @save="createPost" @cancel="close" />
Чому оголошувати:
- документація інтерфейсу: видно, які події компонент генерує, - як props для вхідних даних;
- відсутність подвійних спрацювань: неоголошена подія прокидається як нативний слухач на кореневий елемент. Якщо дитина робить
emit('click'), аclickне оголошено, обробник батька спрацює і від нативного кліку, і відemit- двічі; - TypeScript перевіряє назви й типи параметрів подій.
Валідація параметрів - об'єктний синтаксис:
const emit = defineEmits({
save: (payload) => {
if (!payload?.title) {
console.warn('Подія save без title');
return false;
}
return true;
},
cancel: null, // без перевірки
});
Невдала перевірка лише виводить попередження в режимі розробки - подія все одно відправляється. Це інструмент налагодження, а не захист.
З TypeScript - типова сигнатура, що краще за валідатори:
const emit = defineEmits<{
save: [payload: { title: string }];
cancel: [];
}>();
Іменування: у скрипті - camelCase (emit('itemSelected')), у шаблоні батька можна писати kebab-case (@item-selected) - Vue перетворює автоматично.
Чим події компонентів відрізняються від подій DOM:
- не спливають - подію дитини чує лише її безпосередній батько. Для передачі через кілька рівнів - повторний
emitна кожному рівні,provide/injectз функцією або стор; - синхронні: обробники батька виконуються в момент
emit.
emit у setup поза <script setup> - другий аргумент setup(props, { emit }).
Повна форма директиви: v-назва:аргумент.модифікатор="значення". Наприклад, v-bind:href="url", v-on:submit.prevent="save".
Динамічний аргумент - назва атрибута чи події з виразу в квадратних дужках:
<a :[attributeName]="url">...</a>
<button @[eventName]="handler">...</button>
Якщо attributeName = 'href', це те саме, що :href="url". Значення null прибирає прив'язку.
Обмеження динамічних аргументів:
- вираз має давати рядок або
null; - без пробілів і лапок усередині дужок:
:['data-' + key]- синтаксична помилка. Складні назви обчислюють уcomputed; - у шаблонах прямо в HTML-сторінці (не в
.vue-файлах) браузер переводить атрибути в нижній регістр::[someAttr]стане:[someattr]і не знайде змінну.
Прив'язка об'єкта атрибутів - v-bind без аргументу:
<input v-bind="{ id: 'email', type: 'email', required: true }">
<BaseInput v-bind="field" /> <!-- розгорнути об'єкт у props/атрибути -->
Скорочення з однаковою назвою (Vue 3.4+): якщо атрибут і змінна називаються однаково, значення можна опустити:
<img :src :alt> <!-- те саме, що :src="src" :alt="alt" -->
<UserCard :user :role />
Kebab-case атрибут шукає camelCase змінну: :data-id → dataId.
Логічні атрибути: :disabled="isSaving" - атрибут з'являється при істинному значенні й зникає при хибному (false, null, undefined). Порожній рядок '' для логічних атрибутів вважається істинним, як у HTML.
Модифікатори v-bind:
.prop- встановити як властивість DOM, а не атрибут (:value.prop, скорочено.value);.attr- навпаки, примусово як атрибут (:width.attr, скорочено^width);.camel- перетворити kebab-case на camelCase (для атрибутів SVG на кшталтviewBoxу HTML-шаблонах).
Чому це корисно на практиці: спільні компоненти-обгортки (поле форми, кнопка-посилання), що приймають набір атрибутів ззовні, і генерація форм з конфігурації, де назви полів і подій приходять з даних.
Vue сам керує DOM, але інколи потрібен прямий доступ до елемента: фокус, прокрутка, вимір розміру, ініціалізація сторонньої бібліотеки. Для цього - template ref.
Vue 3.5+ - useTemplateRef:
<script setup>
import { useTemplateRef, onMounted } from 'vue';
const input = useTemplateRef('search');
onMounted(() => input.value.focus());
</script>
<template>
<input ref="search" type="search">
</template>
Рядок у useTemplateRef('search') збігається зі значенням атрибута ref.
До 3.5 - ref з тією самою назвою, що й атрибут:
const search = ref(null); // <input ref="search">
Цей спосіб працює й далі, але useTemplateRef явніший: видно, що змінна - саме посилання на елемент, і не потрібно збігу назв змінних.
Коли ref заповнений:
- після монтування - в
setupіonBeforeMountвінnull; - під
v-ifстаєnull, коли елемент прибрано, - звідси перевіркиinput.value?.focus(); - після зміни стану DOM оновлюється асинхронно: щоб звернутися до щойно показаного елемента, потрібен
await nextTick():
async function openEditor() {
editing.value = true;
await nextTick();
editorInput.value.focus();
}
Ref у v-for дає масив елементів (порядок не гарантовано збігається з масивом даних):
const items = useTemplateRef('items'); // <li v-for="..." ref="items">
Ref-функція - для повного контролю: :ref="(el) => { if (el) map.set(id, el) }" викликається з елементом при монтуванні й з null при видаленні.
Ref на компонент дає екземпляр компонента - але з <script setup> доступне лише те, що відкрито через defineExpose.
Коли НЕ треба ref: змінювати вміст, класи, атрибути напряму через DOM (el.textContent = ..., el.classList.add). Наступний рендер Vue перезапише зміни, а стан стане неузгодженим. Усе, що можна виразити через стан і шаблон, - через стан і шаблон.
Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в DOM усередині батьків, CSS батьків ламає позиціонування:
transform,filter,perspectiveна предку роблятьposition: fixedвідносним до цього предка, а не до вікна;overflow: hiddenобрізає вікно;z-indexобмежений контекстом накладання (stacking context) батька.
<Teleport> рендерить вміст в інше місце DOM, лишаючи його логічно частиною компонента:
<script setup>
const open = ref(false);
</script>
<template>
<button @click="open = true">Видалити акаунт</button>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-50 grid place-items-center bg-black/50">
<div role="dialog" aria-modal="true" class="rounded bg-white p-6">
<p>Ви впевнені?</p>
<button @click="open = false">Скасувати</button>
</div>
</div>
</Teleport>
</template>
Що зберігається: реактивність, props, події, provide/inject - вміст лишається дочірнім у дереві компонентів. Змінюється лише місце в DOM.
Параметри:
to- CSS-селектор чи елемент; ціль має існувати на момент монтування;:disabled="isMobile"- рендерити на місці (наприклад, на мобільних вікно стає звичайним блоком);defer(Vue 3.5+) - відкласти пошук цілі до кінця поточного монтування, щоб ціллю міг бути елемент, який Vue рендерить пізніше в тому самому дереві.
Кілька Teleport в одну ціль дописують вміст по черзі - зручно для стеку сповіщень.
Що ще треба для якісного модального вікна (Teleport цього не робить):
- фокус: перевести фокус у вікно при відкритті, втримувати його всередині (focus trap) і повертати на кнопку після закриття;
- закриття по Escape і кліку на підкладку;
- блокування прокрутки сторінки під вікном;
role="dialog",aria-modal,aria-labelledbyдля зчитувачів екрана.
Нативна альтернатива - елемент <dialog> з showModal(): браузер сам дає верхній шар (top layer, без проблем із z-index), фокус і Escape. У сучасних застосунках це часто простіше за Teleport.
SSR: телепортований вміст рендериться окремо - його треба вставити в HTML вручну, інакше буде розбіжність при гідратації.
<Transition> анімує один елемент чи компонент, коли той з'являється або зникає через v-if, v-show, зміну динамічного компонента чи key.
<Transition name="fade">
<p v-if="visible">Збережено</p>
</Transition>
<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>
Класи, які Vue додає по черзі (name - префікс):
-enter-from→-enter-active→-enter-to- поява;-leave-from→-leave-active→-leave-to- зникнення.
Vue сам визначає, коли анімація закінчилася (за transitionend/animationend), і лише тоді прибирає елемент з DOM.
З Tailwind - класи напряму через props:
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
leave-active-class="transition duration-150 ease-in"
leave-to-class="opacity-0"
>
Режими для заміни одного елемента іншим: mode="out-in" - спершу зникає старий, потім з'являється новий (без накладання).
<TransitionGroup> - для списків (v-for): анімує додавання, видалення й переміщення елементів:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</TransitionGroup>
<style>
.list-move { transition: transform 0.3s; } /* плавне переміщення при зміні порядку */
.list-leave-active { position: absolute; } /* щоб сусіди плавно зсувалися */
</style>
Обов'язково :key на кожному елементі - без нього Vue не знає, що анімувати.
JavaScript-хуки (@before-enter, @enter, @leave з колбеком done) - для анімацій бібліотеками (GSAP, Web Animations API).
Пастки:
<Transition>очікує рівно один кореневий елемент - компонент з кількома коренями всередині не анімується;- анімація не запускається при першому рендері - для цього атрибут
appear; prefers-reduced-motion- користувачі, які вимкнули анімації в системі, мають їх не бачити:@media (prefers-reduced-motion: reduce)чиmotion-reduce:у Tailwind;- для переходів між сторінками в сучасних браузерах можна використати View Transitions API.
Navigation guards - функції, що виконуються перед переходом і можуть його дозволити, скасувати чи перенаправити.
Глобальний guard з перевіркою meta маршруту:
const routes = [
{ path: '/login', name: 'login', component: Login },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
];
router.beforeEach((to, from) => {
const auth = useAuthStore();
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } };
}
// нічого не повертати (або true) - перехід дозволено
});
Що може повернути guard:
undefined/true- продовжити;false- скасувати перехід;- маршрут (рядок чи об'єкт) - перенаправити;
- Promise - роутер дочекається результату (async-guard з запитом до API).
Третій аргумент next досі підтримується, але документація радить повертати значення: з next легко забути виклик чи викликати його двічі.
meta успадковується: перевірка to.meta.requiresAuth спрацює й для вкладених маршрутів, якщо мета задана на батьківському (точніше - to.matched.some(r => r.meta.requiresAuth) враховує всі рівні).
Інші guards:
beforeEnterу конфігурації маршруту - лише для нього;onBeforeRouteLeaveу компоненті - попередити про незбережені зміни;onBeforeRouteUpdate- коли змінюються параметри того самого маршруту;router.afterEach- аналітика, заголовок сторінки (не може скасувати перехід).
Головне застереження: guard - це зручність інтерфейсу, а не захист. Код сторінки вже завантажено в браузер, а будь-хто може змінити стан стору в devtools. Справжній захист - на сервері: API має перевіряти автентифікацію й права на кожен запит.
Пастка нескінченного перенаправлення: guard, що відправляє на /login, не повинен застосовуватися до самого /login.
При переході між маршрутами з тим самим компонентом (/users/1 → /users/2) Vue Router не перестворює компонент - він лише оновлює route.params. Хуки onMounted і setup не виконуються вдруге, і дані першого користувача лишаються на екрані.
<script setup>
const route = useRoute();
const user = ref(null);
onMounted(async () => {
user.value = await fetchUser(route.params.id); // лише один раз!
});
</script>
Рішення 1 - стежити за параметром:
watch(
() => route.params.id,
async (id) => { user.value = await fetchUser(id); },
{ immediate: true },
);
Рішення 2 - guard оновлення в компоненті:
onBeforeRouteUpdate(async (to) => {
user.value = await fetchUser(to.params.id);
});
Рішення 3 - примусово перестворювати компонент через key:
<RouterView :key="$route.fullPath" />
Просто, але дорого: при кожній зміні query перемонтовується вся сторінка, губиться її стан.
Параметри як props - компонент не залежить від роутера:
{ path: '/users/:id', component: UserProfile, props: true }
// перетворення: число замість рядка, плюс query
{
path: '/search',
component: SearchPage,
props: (route) => ({ query: route.query.q, page: Number(route.query.page ?? 1) }),
}
<script setup>
const props = defineProps({ id: String });
watch(() => props.id, loadUser, { immediate: true });
</script>
Переваги props:
- компонент можна використати поза роутером і легко тестувати - достатньо передати props;
- приведення типів і значення за замовчуванням в одному місці (функція
props).
Проблема «компонент не оновився» з props не зникає - стежити за зміною все одно треба, але через props.id.
На відміну від Vuex, у Pinia немає мутацій - стан можна змінювати кількома способами.
1. Пряме присвоєння - найпростіше:
const cart = useCartStore();
cart.coupon = 'SALE10';
cart.items.push(item);
2. $patch з об'єктом - кілька полів за раз:
cart.$patch({ coupon: 'SALE10', deliveryMethod: 'courier' });
3. $patch з функцією - для масивів і складних змін:
cart.$patch((state) => {
state.items = state.items.filter((i) => i.qty > 0);
state.updatedAt = Date.now();
});
Об'єктна форма для масивів незручна: щоб видалити елемент, довелося б передати весь новий масив. Функція також об'єднує кілька змін в один запис у devtools.
4. Дії - для змін з логікою, перевірками чи запитами до API. Правило: якщо зміна має бізнес-сенс («оформити замовлення»), вона - дія стору, а не набір присвоєнь у компоненті.
$reset() - повернути початковий стан:
- в option store вбудований: викликає
state()заново; - у setup store його немає - пишуть самі:
export const useFiltersStore = defineStore('filters', () => {
const initial = () => ({ city: null, salaryFrom: null });
const filters = ref(initial());
function $reset() {
filters.value = initial();
}
return { filters, $reset };
});
$subscribe - реагувати на будь-яку зміну стану:
cart.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state.items));
}, { detached: true });
- за замовчуванням підписка знімається разом із компонентом;
detached: true- лишити; flush: 'sync'- викликати після кожної зміни, а не раз після групи;- для дій є окремий
$onAction- до і після виконання, з обробкою помилок.
Замінити стан повністю - pinia.state.value = {...} (для SSR і тестів), а не cart.$state = ... у звичайному коді.
Vue Router підтримує кілька способів зберігати маршрут в URL.
createWebHistory() - звичайні адреси через History API:
https://example.com/users/7
- красиві URL, які індексують пошукові системи;
- потребує налаштування сервера. Перехід усередині застосунку обробляє JavaScript, але якщо користувач оновить сторінку чи відкриє посилання напряму, браузер запитає в сервера
/users/7. Сервер, який про такий маршрут не знає, поверне 404.
createWebHashHistory() - маршрут після #:
https://example.com/#/users/7
- частина після
#на сервер не надсилається - сервер завжди віддаєindex.html, нічого налаштовувати не треба; - погано для SEO і виглядає застаріло. Доречно для внутрішніх інструментів, статичних хостингів без налаштувань, Electron.
createMemoryHistory() - без URL узагалі: для SSR на сервері й тестів.
Налаштування сервера для createWebHistory - будь-який невідомий шлях віддає точку входу SPA:
location / {
try_files $uri $uri/ /index.html;
}
У Laravel, якщо SPA живе всередині застосунку:
// routes/web.php - останнім, після всіх інших маршрутів
Route::view('/app/{any?}', 'spa')->where('any', '.*');
Пастки:
- справжні 404 зникають: сервер на будь-яку адресу віддає 200 з SPA. Сторінку «не знайдено» робить роутер (
/:pathMatch(.*)*), але HTTP-статус лишається 200 - для SEO це «м'які 404»; - catch-all маршрут має бути останнім і не перехоплювати
/api/*, інакше запити до API отримають HTML; base- якщо застосунок не в корені домену:createWebHistory('/app/'), і той самий префікс у налаштуваннях збирача.
Деякі дані потрібні майже на кожній сторінці: поточний користувач у шапці, назва застосунку, налаштування. Передавати їх у кожному контролері незручно - для цього є спільні дані в middleware HandleInertiaRequests:
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return [
...parent::share($request),
'appName' => config('app.name'),
'auth' => [
'user' => fn () => $request->user()?->only('id', 'name', 'avatar_url'),
],
];
}
}
<script setup>
import { computed } from 'vue';
import { usePage } from '@inertiajs/vue3';
const page = usePage();
const user = computed(() => page.props.auth.user);
</script>
Спільні дані зливаються з props сторінки - тож простір імен (auth.user) захищає від зіткнень із props контролерів.
Головні правила:
- спільні дані йдуть з кожною відповіддю - кожен перехід і кожне часткове перезавантаження. Важкий запит тут - податок на всі сторінки. Замикання (
fn () => ...) обчислюються лише тоді, коли prop справді потрібен; - рідко змінні дані (список країн, налаштування) -
Inertia::once()чи методshareOnce(): клієнт запам'ятовує значення й не отримує його з кожною відповіддю; - усе видно в браузері.
$request->user()безonly()віддасть усі атрибути моделі в JSON кожної сторінки - email, телефон, службові прапорці, усе, що не в$hidden. Передавайте явний перелік полів чи ресурс; - ролі й права - готовими булевими значеннями для інтерфейсу (
can.manageUsers), а не списком усіх дозволів системи.
Валідаційні помилки адаптер Laravel уже додає в спільні дані (errors), а флеш-дані в Inertia 3 мають окремий механізм Inertia::flash().
Версія ассетів визначається в тому самому middleware (метод version()) - для Vite вона обчислюється автоматично.
У класичній SPA сервер на помилку валідації повертає 422 з JSON, а клієнт розкладає помилки по полях. Inertia працює інакше - так само, як звичайні форми Laravel:
- форма відправляється Inertia-запитом;
$request->validate()не проходить - Laravel перенаправляє назад і кладе помилки в сесію;- адаптер Inertia додає помилки з сесії в props сторінки (
errors); - Inertia бачить непорожній
page.props.errorsі вважає відправку невдалою - викликаєonError, а неonSuccess.
Відповідей 422 Inertia-застосунок не генерує взагалі.
public function store(Request $request)
{
$request->validate([
'email' => ['required', 'email', 'unique:users'],
'name' => ['required', 'max:100'],
]);
// ...
}
З useForm помилки потрапляють у form.errors автоматично. Без нього - через prop:
<script setup>
defineProps({ errors: Object });
</script>
<template>
<p v-if="errors.email">{{ errors.email }}</p>
</template>
Введене не губиться: після перенаправлення назад Inertia за замовчуванням зберігає стан компонента сторінки, тож поля не очищаються.
Error bags - для сторінки з кількома формами з однаковими назвами полів (дві форми з полем email):
router.post('/companies', data, { errorBag: 'createCompany' });
router.post('/users', data, { errorBag: 'createUser' });
// помилки прийдуть у page.props.errors.createCompany / .createUser
З useForm error bags не потрібні - помилки й так прив'язані до екземпляра форми.
Усі помилки поля, а не лише перша. За замовчуванням адаптер Laravel повертає по одній помилці на поле. Для всіх - protected $withAllErrors = true; у HandleInertiaRequests; тоді кожне поле містить масив рядків.
Часткові перезавантаження й помилки. errors передається завжди, тож порожній набір помилок з сервера затре клієнтські. Щоб зберегти помилки під час router.reload({ only: [...] }), у Inertia 3 є опція preserveErrors: true.
Попередня валідація (до відправки) - Laravel Precognition, підтримку якої вбудовано у форми Inertia.
Сторінка списку користувачів має props users і companies (для фільтра). Користувач змінює фільтр - потрібні нові users, а companies не змінилися. Отримувати й перераховувати все - марна робота.
Часткове перезавантаження - запит лише вибраних props тієї самої сторінки:
import { router } from '@inertiajs/vue3';
router.reload({ only: ['users'], data: { company: 3 } });
router.reload({ except: ['companies'] });
<Link href="/users?active=1" :only="['users']">Активні</Link>
Працює лише для візитів на той самий компонент сторінки.
Щоб сервер справді не виконував зайве - props мають бути замиканнями:
return Inertia::render('Users/Index', [
'users' => fn () => User::query()->filter(request()->all())->paginate(),
'companies' => fn () => Company::orderBy('name')->get(['id', 'name']),
]);
Без замикання Company::all() виконається на кожен запит, навіть якщо результат не потрапить у відповідь.
Типи props:
| Запис | Звичайний візит | Часткове перезавантаження | Обчислюється |
|---|---|---|---|
User::all() |
так | за запитом | завжди |
fn () => User::all() |
так | за запитом | лише коли потрібен |
Inertia::optional(fn () => ...) |
ні | за запитом | лише коли потрібен |
Inertia::always(...) |
так | завжди | завжди |
Inertia::optional()- prop, який сторінка отримує лише на явний запит (only): дані для вкладки, яку відкривають рідко. У Inertia 3 він замінив видаленийInertia::lazy();Inertia::always()- потрапляє навіть у часткові перезавантаження (так передаються помилки валідації).
Inertia 3: only/except підтримують крапкову нотацію для вкладених props (only: ['auth.notifications']), а optional, defer, merge працюють на будь-якій глибині масиву.
Поєднання з once(): Inertia::optional(fn () => ...)->once() - завантажити один раз і пам'ятати на клієнті між переходами.
Докладніше в документації: Inertia: часткові перезавантаження
Повідомлення «Користувача створено» має з'явитися один раз після дії й не повторюватися. Раніше в Inertia-застосунках його передавали через session()->flash() і спільні дані middleware. У Inertia 3 для цього є окремий механізм - флеш-дані.
На сервері:
use Inertia\Inertia;
public function store(StoreUserRequest $request)
{
$user = User::create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Користувача створено']);
return back();
// або коротше: return Inertia::flash('newUserId', $user->id)->back();
}
flash() можна поєднувати і з render(): Inertia::render(...)->flash('highlight', $id).
На клієнті - у page.flash, окремо від props:
<script setup>
import { usePage } from '@inertiajs/vue3';
const page = usePage();
</script>
<template>
<div v-if="page.flash.toast" class="toast">{{ page.flash.toast.message }}</div>
</template>
Або реагувати подією - в одному місці макета:
router.on('flash', (event) => {
if (event.detail.flash.toast) showToast(event.detail.flash.toast);
});
router.post('/users', data, {
onFlash: ({ newUserId }) => highlight(newUserId),
});
Чим кращий за флеш через сесію в props:
- не потрапляє в історію браузера. Props зберігаються в стані історії: користувач натиснув «Назад» - і старе «Збережено» знову на екрані. Флеш-дані в історію не пишуться;
- не змішується з props сторінки;
- працює однаково з редиректом і без нього: middleware сам зберігає флеш у сесію при перенаправленні.
Що варто знати:
- обробники
router.on(...), зареєстровані в компонентах, треба знімати при демонтажі (функція, яку повертаєon), інакше в непостійних макетах вони накопичуються; - флеш - для одноразових повідомлень, а не для даних, які мають бути на сторінці після оновлення;
- як і props, флеш-дані видно в браузері - лише публічна інформація.
defineEmits оголошує, які події компонент може випромінювати і з якими аргументами.
Сучасний синтаксис (Vue 3.3+) - іменовані кортежі:
<script setup lang="ts">
const emit = defineEmits<{
change: [id: number]
update: [value: string, source: 'keyboard' | 'paste']
close: []
}>()
emit('change', 42)
emit('update', 'текст', 'paste')
emit('change', 'abc') // помилка TypeScript
emit('unknown') // помилка TypeScript
</script>
Старіший синтаксис - сигнатури викликів, працює так само:
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'close'): void
}>()
Батьківський компонент отримує перевірку типів у шаблоні: @change="(id) => select(id)" - id має тип number, а підписка на неоголошену подію підсвічується редактором (з розширенням Vue - Official і vue-tsc).
Навіщо оголошувати події взагалі:
- документація компонента: з першого рядка видно його «вихідний» інтерфейс;
- атрибути-слухачі не протікають на корінь: оголошена подія не потрапляє у
$attrs. Неоголошена - потрапляє і може навісити нативний обробник на кореневий елемент, через що@clickспрацює двічі; - перевірка в режимі розробки: з runtime-валідацією (об'єктний синтаксис з функціями-валідаторами) Vue попередить про некоректне корисне навантаження.
Типові помилки:
- події в стилі camelCase і kebab-case: у шаблоні батька пишуть
@item-selected, у дочірньомуemit('itemSelected')- Vue їх зіставляє, але в типах і пошуку по коду краще одна форма; - зміна props замість події: дочірній компонент мутує об'єкт із props («він же не примітив, працює») - потік даних стає непрозорим. Зміну даних батька - лише через подію чи
defineModel; - занадто багато подій - знак, що компонент робить забагато або що частину стану варто винести в стор.
v-model на компоненті - це скорочення для пари «prop + подія оновлення»:
<PriceInput v-model="price" />
<!-- те саме, що -->
<PriceInput :modelValue="price" @update:modelValue="(v) => price = v" />
До Vue 3.4 дочірній компонент мусив оголосити prop, подію і зв'язати їх вручну. defineModel (3.4+) робить це одним рядком:
<!-- PriceInput.vue -->
<script setup lang="ts">
const model = defineModel<number>({ required: true })
</script>
<template>
<input type="number" v-model="model" />
</template>
model - ref: читання повертає значення від батька, присвоєння model.value = 10 випромінює update:modelValue. Компілятор створює і prop modelValue, і подію автоматично.
Кілька моделей і назви:
<!-- батько -->
<DateRange v-model:from="startDate" v-model:to="endDate" />
<!-- DateRange.vue -->
<script setup lang="ts">
const from = defineModel<string>('from')
const to = defineModel<string>('to')
</script>
Модифікатори (v-model.trim) доступні через деструктуризацію:
const [model, modifiers] = defineModel<string>({
set(value) {
return modifiers.trim ? value.trim() : value
},
})
Значення за замовчуванням і розсинхронізація. Якщо модель має default, а батько не передав їй значення (його змінна undefined), дочірній компонент працюватиме зі своїм значенням за замовчуванням: у батька undefined, у дитини, наприклад, 1. Документація прямо попереджає про цю розсинхронізацію - надійніше, щоб початкове значення задавав батько.
Пастки:
model.value.push(x)для масиву змінює об'єкт батька напряму, без події - краще присвоювати новий масив (model.value = [...model.value, x]);v-modelна<input>усередині з тим самимmodel- нормальний і найкоротший шлях обгорнути нативне поле.
Питання з реальних технічних співбесід - 108 питань у 9 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії