Na rozmowach z Vue najczęściej pada kilkanaście pytań: czym jest reaktywność, czym różnią sięrefireactive,computediwatch, jak komponent komunikuje się z rodzicem przez props i emit oraz czym są Composition API i Pinia. Rekruter nie sprawdza definicji z pamięci, tylko to, czy rozumiesz, kiedy Vue odświeża widok, a kiedy traci reaktywność. Dobra odpowiedź to jedno zdanie definicji i krótki przykład kodu.
Rozmowa techniczna z Vue rzadko jest egzaminem z całej dokumentacji. Najczęściej to te same kilkanaście tematów, przy których rekruter sprawdza, czy rozumiesz, jak działa reaktywność, a nie czy pamiętasz nazwy dyrektyw. Poniżej znajdziesz pytania, które wracają na rozmowach na stanowisko juniora, razem z tym, jak na nie odpowiadać. Jeśli dopiero poznajesz ten framework, zacznij od artykułu o tym, czym jest Vue i kiedy wybrać go zamiast Reacta, a ten tekst potraktuj jako listę kontrolną. Vue stoi na JavaScripcie (często z TypeScriptem), więc przy rekrutacji padają też pytania rekrutacyjne z JavaScript i z TypeScript.
Przykłady z tego artykułu sprawdziliśmy w projekcie na Vue 3.5 z TypeScriptem: komponenty przeszły kontrolę typów (vue-tsc), a zachowanie reaktywności uruchomiliśmy w Node. Komunikaty i wyniki w tekście pochodzą z tych uruchomień.
Jak odpowiadać na pytania rekrutacyjne z Vue?
Najlepiej w trzech krokach: jedno zdanie definicji, krótki przykład (kilka linijek kodu albo sytuacja z projektu) i informacja, kiedy to ma znaczenie w praktyce. Taka odpowiedź zajmuje 30–60 sekund i pokazuje, że rozumiesz mechanizm, a nie wkułeś formułkę. Jeśli czegoś nie wiesz, powiedz, co wiesz, i rozumuj na głos. Rekruterzy oceniają sposób myślenia, a nie tylko wynik.
Warto też wiedzieć, że Vue ma dwa style pisania komponentów. Starszy to Options API (obiekt z data, methods, computed), nowszy to Composition API z <script setup>. Dokumentacja poleca Composition API dla nowych projektów, ale w pracy trafisz na oba. Dlatego przy kluczowych pytaniach podaję obie perspektywy.
Pytania są ułożone od podstaw do tematów, które częściej pojawiają się przy ofertach na mid-poziom.
Podstawy Vue
Czym jest Vue i czym różni się od Reacta?
Vue to framework JavaScript do budowy interfejsów użytkownika, oparty na komponentach i reaktywności. Nazywa się go frameworkiem progresywnym, bo możesz użyć go do jednego widżetu na stronie albo do całej aplikacji. React jest biblioteką do widoków i opiera się na JSX, a Vue dostarcza własny szablon HTML z dyrektywami (v-if, v-for) oraz oficjalne rozwiązania do routingu (Vue Router) i stanu (Pinia). Szerzej opisujemy to w artykule o tym, czym jest Vue.
Co to jest komponent jednoplikowy (SFC)?
Komponent jednoplikowy (single-file component, SFC) to plik .vue, który trzyma w jednym miejscu trzy części: logikę w <script>, szablon w <template> i style w <style>. Narzędzie budujące (Vite) dzieli go na kod JavaScript i CSS. Dzięki temu wszystko, co dotyczy jednego komponentu, leży obok siebie, a style mogą być ograniczone do tego komponentu przez <style scoped>.
Czym są dyrektywy w Vue?
Dyrektywa to specjalny atrybut z prefiksem v-, który mówi Vue, co zrobić z elementem. Najczęściej używane to:
| Dyrektywa | Skrót | Do czego służy |
|---|---|---|
v-bind:src="url" | :src="url" | ustawia atrybut lub właściwość z wyrażenia |
v-on:click="save" | @click="save" | podpina obsługę zdarzenia |
v-model | brak | dwukierunkowe wiązanie pola formularza |
v-if / v-else | brak | wyświetla element warunkowo |
v-for | brak | powtarza element dla każdego elementu listy |
v-show | brak | pokazuje lub ukrywa element przez CSS |
Skróty : i @ to ten sam zapis, tylko krótszy, więc @click i v-on:click robią to samo.
Jak wyświetlać warunkowo i listy?
<p v-if="count > 2">Dużo!</p>
<p v-else>Mało</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
Przy v-for zawsze podajesz :key z unikalnym identyfikatorem. Dzięki temu Vue przy zmianie listy przestawia istniejące elementy DOM, zamiast tworzyć je od nowa, i nie myli stanu elementów (np. wpisanego tekstu w polach).
Czym różni się v-if od v-show?
v-if dodaje lub usuwa element z DOM, a v-show zawsze go renderuje i tylko przełącza display: none. Dlatego v-if jest tańszy na starcie, gdy warunek najczęściej jest fałszywy, a v-show jest tańszy, gdy element przełączasz bardzo często. Jest też różnica w zachowaniu: po v-if komponent jest tworzony od nowa i traci swój stan, a po v-show stan zostaje. v-show nie działa z <template> ani z v-else.
Czym różni się v-model od v-bind?
v-bind przekazuje wartość w jedną stronę: ze zmiennej do elementu. v-model łączy w obie strony: wartość trafia do pola, a to, co użytkownik wpisze, wraca do zmiennej. W rzeczywistości v-model to skrót dla :value plus @input, co warto powiedzieć na rozmowie, bo pokazuje, że rozumiesz mechanizm.
Reaktywność
Czym jest reaktywność w Vue?
Reaktywność to mechanizm, dzięki któremu Vue samo wie, że zmieniła się dana, i odświeża tylko te fragmenty szablonu, które z niej korzystają. Nie wywołujesz żadnej metody typu „odśwież widok”: zmieniasz wartość, a reszta dzieje się automatycznie. W Vue 3 działa to przez obiekty Proxy, które śledzą odczyty i zapisy właściwości.
Czym różni się ref od reactive?
ref opakowuje dowolną wartość (liczbę, napis, obiekt) w obiekt z właściwością .value. reactive zwraca reaktywny obiekt i nie wymaga .value, ale działa tylko na obiektach i tablicach.
import { reactive, ref } from 'vue'
const count = ref(0)
count.value++ // w skrypcie potrzebujesz .value
const state = reactive({ n: 1 })
state.n++ // obiekt bez .value
W szablonie .value pomijasz, bo Vue rozpakowuje ref automatycznie ({{ count }}). Gdy zapomnisz o .value w skrypcie, TypeScript to wychwyci. W naszym teście count + 1 dla ref(0) dało błąd TS2365: „Operator '+' cannot be applied to types 'Ref<number, number>' and 'number'”. Dokumentacja i większość zespołów zaleca ref jako domyślny wybór, bo działa na każdym typie i daje spójny zapis.
Dlaczego destrukturyzacja obiektu reactive gubi reaktywność?
Bo wyciągnięta wartość to zwykła liczba, a nie połączenie z obiektem. Sprawdziliśmy: po const { n } = state zmiana state.n = 2 zostawia n równe 1. Rozwiązaniem jest toRefs(state), który zamienia każde pole w ref połączony z oryginałem. W naszym teście po zmianie toRefs pokazał 2, a zwykła destrukturyzacja dalej 1.
import { reactive, toRefs } from 'vue'
const state = reactive({ n: 1 })
const { n } = toRefs(state) // n to ref, n.value === state.n
state.n = 2 // n.value === 2
To klasyczne pytanie z drugiej rundy: dobra odpowiedź nie kończy się na „reaktywność znika”, tylko wyjaśnia dlaczego (wartość prosta jest kopiowana) i jak to naprawić.
Czym różni się computed od metody?
Obie wartości możesz wywołać w szablonie, ale computed zapamiętuje wynik i przelicza go tylko wtedy, gdy zmieni się któraś z użytych w nim wartości reaktywnych. Metoda wykonuje się przy każdym renderowaniu. Sprawdziliśmy to: computed odczytany dwa razy wywołał funkcję jeden raz, a ponownie dopiero po zmianie zależności.
import { computed, ref } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
computed powinien być czysty: tylko wylicza wartość z innych danych i nie zmienia stanu ani nie robi zapytań HTTP.
Czym różni się watch od watchEffect?
watch obserwuje wskazane źródło i daje dostęp do nowej oraz poprzedniej wartości. watchEffect uruchamia się od razu i sam śledzi wszystko, co odczyta w środku.
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
watch(count, (now, before) => console.log(before, '->', now))
watchEffect(() => console.log('count =', count.value))
Ciekawostka z naszego testu: po dwóch szybkich zmianach w.value = 1; w.value = 2 watch wywołał się raz, z przejściem 0 -> 2. Vue grupuje zmiany i uruchamia obserwatorów po zakończeniu bieżącego kroku, więc nie zobaczysz wartości pośredniej. Zasada wyboru: computed, gdy liczysz nową wartość do wyświetlenia, watch, gdy potrzebujesz efektu ubocznego w reakcji na konkretną zmianę (zapytanie do API, zapis do localStorage).
Jak obserwować zagnieżdżone dane? (watch deep)
Domyślnie watch na właściwości, która jest obiektem, reaguje tylko na podmianę całego obiektu. Zmiana pola w środku go nie uruchamia. Sprawdziliśmy: po obj.user.name = 'Ola' obserwator płytki milczał, a obserwator z opcją { deep: true } zadziałał.
watch(() => obj.user, (user) => console.log(user.name), { deep: true })
To jedno z najczęściej oglądanych pytań o Vue na Stack Overflow (ponad 640 tys. wyświetleń), więc na rozmowie bardzo prawdopodobne. Dodaj, że deep kosztuje, bo Vue przechodzi przez całą strukturę, więc lepiej obserwować konkretne pole, np. () => obj.user.name.
Co to jest nextTick?
nextTick zwraca obietnicę, która rozwiązuje się po tym, jak Vue zaktualizuje DOM po zmianie danych. Aktualizacja DOM nie dzieje się natychmiast po zmianie wartości, tylko jest grupowana. Jeśli po zmianie danych chcesz odczytać nowy stan DOM (np. wysokość elementu), poczekaj na await nextTick().
Komponenty i komunikacja
Jak przekazać dane z rodzica do dziecka i z powrotem?
Rodzic przekazuje dane przez props, a dziecko informuje rodzica zdarzeniem (emit).
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{ label: string; start?: number }>()
const emit = defineEmits<{ changed: [value: number] }>()
const count = ref(props.start ?? 0)
function increment() {
count.value++
emit('changed', count.value)
}
</script>
<template>
<button @click="increment">{{ label }}: {{ count }}</button>
</template>
Rodzic użyje go tak: <Counter label="Kliknięcia" @changed="onChange" />. Zasada brzmi: dane płyną w dół, zdarzenia w górę.
Dlaczego nie wolno zmieniać propsa w komponencie dziecka?
Bo props należą do rodzica, a przepływ danych ma być jednokierunkowy. Gdyby dziecko zmieniało props, trudno byłoby wskazać, skąd wzięła się dana wartość. Vue ostrzega o tym w konsoli. Jeśli dziecko potrzebuje własnej, zmiennej wersji, kopiuje wartość do lokalnego ref (jak start w przykładzie), a jeśli ma zmienić dane rodzica, wysyła zdarzenie przez emit.
Jak działa v-model na własnym komponencie?
Od Vue 3.4 używa się do tego makra defineModel, które zamienia prop i zdarzenie w jedną zmienną:
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
Rodzic napisze po prostu <MyInput v-model="name" />. W starszym kodzie robiło się to ręcznie: prop modelValue i zdarzenie update:modelValue. defineModel pod spodem robi dokładnie to samo.
Jak wykryć zmianę propsa?
Obserwuj go przez watch na funkcji zwracającej wartość: watch(() => props.label, (nowa) => ...). Nie przekazuj samego props.label do watch, bo to zwykła wartość (napis), a nie źródło reaktywne. Jeśli chcesz tylko wyliczyć coś z propsa, wystarczy computed.
Jakie są hooki cyklu życia i które trzeba znać?
Hooki cyklu życia (lifecycle hooks) to funkcje, które Vue wywołuje w określonych momentach istnienia komponentu. W Composition API wywołujesz je w setup:
| Hook | Kiedy | Do czego służy |
|---|---|---|
onBeforeMount | przed wstawieniem do DOM | rzadko potrzebny |
onMounted | po wstawieniu do DOM | pobranie danych, praca z elementami DOM, timery |
onUpdated | po aktualizacji DOM | reakcja na odświeżenie widoku (ostrożnie) |
onUnmounted | po usunięciu komponentu | sprzątanie: timery, nasłuchiwacze, subskrypcje |
W Options API odpowiadają im created i mounted. Różnica między nimi (created vs mounted) polega na tym, że w created DOM jeszcze nie istnieje, więc nie sięgniesz do elementów, a w mounted już tak.
import { onMounted, onUnmounted } from 'vue'
let timer: number
onMounted(() => { timer = window.setInterval(() => {}, 1000) })
onUnmounted(() => clearInterval(timer))
Zasada „co zaczynasz w onMounted, kończysz w onUnmounted” to częsta odpowiedź na pytanie o wycieki pamięci.
Do czego służą sloty?
Slot pozwala przekazać do komponentu fragment szablonu, który komponent wstawi w wyznaczonym miejscu. Dzięki temu budujesz komponenty-ramki (karta, modal), których zawartość podaje rodzic.
<!-- Card.vue -->
<div class="card">
<slot>Domyślna treść</slot>
</div>
<!-- użycie -->
<Card><p>Własna treść</p></Card>
Są też sloty nazwane (<slot name="header" /> i <template #header>) oraz sloty z danymi, w których komponent przekazuje rodzicowi wartości do użycia w szablonie.
Jak wywołać metodę komponentu dziecka z rodzica?
Przez referencję do szablonu (template ref): rodzic nadaje dziecku ref="child", a dziecko udostępnia metodę przez defineExpose. W komponentach z <script setup> nic nie jest dostępne z zewnątrz, dopóki tego nie wyeksponujesz. Na rozmowie dodaj, że to rozwiązanie awaryjne, a zwykle lepiej sterować dzieckiem przez props i zdarzenia.
Stan, routing i narzędzia
Czym jest Pinia i po co się jej używa?
Pinia to oficjalna biblioteka do zarządzania stanem współdzielonym między komponentami, następca Vuex. Gdy kilka odległych komponentów potrzebuje tych samych danych (zalogowany użytkownik, koszyk), trzymasz je w sklepie (store), a nie przekazujesz przez wiele poziomów propsów. Sklep może mieć zapis podobny do komponentu:
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useTodos = defineStore('todos', () => {
const todos = ref<string[]>([])
function add(todo: string) {
todos.value.push(todo)
}
return { todos, add }
})
ref działa tu jak stan, funkcje jak akcje, a computed jak gettery. Nie trzeba już rozróżniać mutacji i akcji.
Czym Pinia różni się od Vuex?
Vuex wymagał podziału na stan, gettery, mutacje (jedyne miejsce zmiany stanu) i akcje (asynchroniczne, wywołują mutacje). Pinia ten podział upraszcza: zmieniasz stan bezpośrednio w akcjach, bez mutacji, ma lepsze wsparcie TypeScripta i nie wymaga modułów z przestrzeniami nazw. Vuex jest dziś w trybie utrzymaniowym, a dokumentacja Vue poleca Pinię dla nowych projektów. Pytanie o różnicę padnie głównie wtedy, gdy projekt ma jeszcze Vuex.
Jak działa routing w Vue i czym jest lazy loading?
Vue Router przypisuje adresy URL do komponentów, które wyświetla <RouterView>. Leniwe ładowanie (lazy loading) oznacza, że kod danej strony pobiera się dopiero po wejściu na jej adres, co skraca pierwsze ładowanie aplikacji.
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./Home.vue') },
{ path: '/admin', component: () => import('./Admin.vue') },
],
})
Zapis () => import(...) zamiast zwykłego importu mówi bundlerowi, żeby wydzielił stronę do osobnego pliku.
Co to jest guard nawigacji?
Guard nawigacji (navigation guard) to funkcja, która decyduje, czy użytkownik może wejść na daną trasę, np. router.beforeEach lub beforeEnter przy konkretnej ścieżce. Zwraca true albo adres, na który ma zostać przekierowany. Pamiętaj, że guard poprawia komfort użytkownika, ale nie jest zabezpieczeniem: prawdziwą kontrolę dostępu musi zrobić serwer.
Czym są Composition API i Options API?
Options API grupuje kod według typu: data, methods, computed, watch. Composition API grupuje kod według funkcji, więc cała logika jednej funkcjonalności leży obok siebie, a powtarzalne fragmenty wyciągasz do funkcji zwanych composables (np. useMouse, useFetch). Composition API jest wygodniejsze przy dużych komponentach i lepiej współpracuje z TypeScriptem, a Options API bywa prostsze dla początkujących i nadal działa. W nowych projektach używa się <script setup>.
Co to jest composable i czym różni się od komponentu?
Composable to zwykła funkcja, która używa API reaktywności Vue (ref, computed, hooków) i zwraca stan oraz funkcje do ponownego użycia. Komponent ma szablon i renderuje się w DOM, composable ma tylko logikę. Przykładowo useFetch(url) może zwracać data, error i loading, a każdy komponent, który tego potrzebuje, po prostu je wywołuje.
Testy i praktyka
Jakie zadania z kodem pojawiają się na rozmowach z Vue?
Najczęściej to małe aplikacje do zrobienia w godzinę lub w domu:
- lista zadań z dodawaniem i usuwaniem (komponenty,
v-model,v-for), - wyszukiwarka z zapytaniem do API (
watchlubwatchEffect, obsługa ładowania i błędów), - formularz z walidacją (
v-model,computeddla komunikatów), - dwie podstrony z nawigacją i ochroną jednej z nich (Vue Router, guard).
Poza kodem prosi się też o poprawienie istniejącego fragmentu: znalezienie miejsca, w którym reaktywność przestaje działać (np. po destrukturyzacji), albo wyjaśnienie, dlaczego watch nie reaguje na zmianę w zagnieżdżonym obiekcie.
Jak testuje się komponenty Vue?
Do uruchamiania testów służy zazwyczaj Vitest, a do montowania komponentów i symulowania zdarzeń oficjalna biblioteka Vue Test Utils. Test montuje komponent z podanymi propsami, klika przycisk, sprawdza wyświetlony tekst i emitowane zdarzenia. Na poziom juniora wystarczy wiedzieć, po co jest montowanie i jak sprawdzić efekt kliknięcia.
Co powie kompilator i przeglądarka? Mini-test na rozgrzewkę
Sprawdź, czy bez uruchamiania kodu potrafisz powiedzieć, co się stanie.
1. const { n } = state, gdzie state = reactive({ n: 1 }), a potem state.n = 2. n zostaje równe 1. Destrukturyzacja kopiuje wartość prostą. Użyj toRefs(state).
2. const count = ref(0), a w skrypcie count + 1. Błąd TS2365, bo count jest obiektem Ref. Poprawnie: count.value + 1.
3. watch(() => obj.user, ...), a następnie obj.user.name = 'Ola'. Obserwator się nie uruchomi. Obserwuj () => obj.user.name albo dodaj { deep: true }.
Jak przygotować się do rozmowy z Vue?
Samo czytanie odpowiedzi nie wystarczy, bo pytania uzupełniające szybko pokazują, czy rozumiesz temat. Skuteczniej działa zbudowanie małej aplikacji (lista zadań z wyszukiwarką, sklepem Pinia i jedną chronioną trasą) i głośne tłumaczenie swoich decyzji komuś innemu, na przykład mentorowi. Dla porządku:
| Obszar | Co umieć wyjaśnić własnymi słowami |
|---|---|
| Podstawy | SFC, dyrektywy, v-if vs v-show, v-model vs v-bind, :key |
| Reaktywność | ref vs reactive, toRefs, computed, watch vs watchEffect, deep, nextTick |
| Komponenty | props, emit, defineModel, sloty, hooki cyklu życia |
| Composition API | <script setup>, composables, Options API vs Composition API |
| Stan i routing | Pinia vs Vuex, Vue Router, lazy loading, guard nawigacji |
| Praktyka | zadania z kodem, testy komponentów (Vitest, Test Utils) |
Na czym juniorzy najczęściej tracą punkty?
Nie na trudnych tematach, tylko na trzech drobiazgach, które wychodzą dopiero przy pytaniach uzupełniających:
- Gubienie reaktywności. Destrukturyzacja obiektu
reactivelub przypisanie wartości zrefdo zwykłej zmiennej odcina połączenie z danymi. Pokaż, że wiesz, kiedy to się dzieje i jak użyćtoRefs. - Mylenie
computedzwatch. Odpowiedź „to prawie to samo” kończy temat. Wyjaśnij, żecomputedliczy wartość do wyświetlenia i jest cache’owany, awatchsłuży do efektów ubocznych. - Zmienianie propsów w dziecku. Zamiast
emitlub kopii w lokalnymref, junior modyfikuje props bezpośrednio, co psuje jednokierunkowy przepływ danych.
Jak brzmi dobra odpowiedź, a jak słaba?
Weźmy pytanie o ref i reactive:
| Odpowiedź | Dlaczego | |
|---|---|---|
| Słaba | „ref i reactive to to samo, tylko ref ma .value.” | Brak różnicy w zachowaniu. Pytanie uzupełniające („co się stanie po destrukturyzacji reactive?”) od razu to pokaże. |
| Dobra | „ref opakowuje dowolną wartość i wymaga .value w skrypcie, reactive działa tylko na obiektach, ale traci reaktywność po destrukturyzacji. Domyślnie używam ref, bo działa na każdym typie.” | Jest różnica w zachowaniu, konsekwencja i uzasadnienie wyboru. |
Ten sam schemat działa przy każdym pytaniu z tego artykułu: definicja, mechanizm, przykład użycia.
Co zrobić, gdy nie znasz odpowiedzi?
Nie zgaduj i nie milcz. Powiedz, co wiesz na pewno, a potem rozumuj na głos, np.: „Nie pamiętam nazwy tego makra, ale wiem, że szukam sposobu na dwukierunkowe wiązanie w moim komponencie, więc sprawdziłbym to w dokumentacji.” Taka odpowiedź pokazuje, że potrafisz sobie poradzić w pracy, a umiejętność znalezienia odpowiedzi jest w codziennej pracy ważniejsza niż pamięć do szczegółów.
Jeśli dopiero planujesz ścieżkę nauki, pamiętaj, że w naszym kursie frontendu uczymy Reacta, bo jest popularniejszy na rynku pracy dla początkujących. Vue wybierz, gdy wymaga go konkretna firma, a przygotowanie do rozmowy z Reactem znajdziesz w pytaniach rekrutacyjnych z React. Ogólny przebieg całego procesu opisujemy w poradniku jak przygotować się do rekrutacji na juniora.


