Ryz Labs/Interview questions/Vue.js developer
Interview questions

Vue.js interview questions for senior developers (2026)

Vue 3.5 questions on reactivity, composables, Pinia and Nuxt, built around real bugs and performance problems rather than API trivia.

These 27 Vue.js interview questions target developers building production apps with Vue 3.5, the Composition API and <script setup>, Pinia for state, Nuxt 3 or 4 for server rendering, and Vite for tooling. They cover reactivity in depth, composables, rendering performance, hydration and the migration work many teams still have in front of them. Ryz vets Vue developers on the same ground: recruiters source engineers with shipped Vue work, structured NTRVSTA AI interviews press on scenarios like these, and recruiters review each candidate before and after. AI scores are advisory; people decide.

How to use these questions

For a mid-level developer, use the fundamentals and intermediate sections and one reactivity question. For a senior developer, spend most of the time on reactivity, rendering and Nuxt, where real bugs come from. If your codebase still uses the Options API or Vue 2, include the migration question; the answer shows whether the candidate has done that work or only read about it.

Ask candidates to write small snippets. Vue code that looks right but silently loses reactivity is the most common bug in Vue codebases, and you only catch it by reading what people write.

Fundamentals

When do you use ref and when reactive, and why does destructuring a reactive object break things?

ref wraps any value, including primitives, and is read through .value in script. reactive makes an object deeply reactive through a proxy but can't hold primitives or be reassigned wholesale. Destructuring a reactive object copies plain values out of the proxy, so they stop tracking; use toRefs if you need to destructure. Many teams default to ref for consistency.

What a strong answer shows: they explain the proxy mechanism, not just the rule.

Explain the difference between computed, watch and watchEffect.

computed derives a cached value from reactive sources and should have no side effects. watch runs a side effect when specific sources change and gives old and new values. watchEffect runs immediately and tracks whatever it reads, which is convenient but makes dependencies implicit.

What a strong answer shows: they reach for computed first and use watchers only for real side effects like fetching or syncing to storage.

How do you build a typed input component that works with v-model?

defineModel declares the model prop and its update event together and returns a ref you can bind or assign.

<script setup lang="ts">
const model = defineModel<string>({ required: true })
defineProps<{ label: string; maxLength?: number }>()
</script>

<template>
<label>
{{ label }}
<input v-model="model" :maxlength="maxLength" />
</label>
</template>

What a strong answer shows: they know a component can expose several named models, like v-model:start and v-model:end.

What is the difference between v-if and v-show, and why do keys matter in v-for?

v-if creates and destroys the element and its component state; v-show toggles CSS display and keeps it mounted. Use v-show for frequent toggles. In v-for, a stable, unique key lets Vue move existing elements instead of patching them in place, which matters for inputs and component state.

What a strong answer shows: they explain the bug an index key causes when items are removed from the middle of a list of inputs.

Which lifecycle hooks run during server-side rendering?

During SSR, only setup and onServerPrefetch run; onMounted, onUpdated and onUnmounted run only in the browser. Code that touches window, document or browser APIs belongs in onMounted or behind a client check.

What a strong answer shows: they connect this to memory leaks on the server when timers are started in setup.

Options API or Composition API with script setup: what do you choose for a new app?

For new code, the Composition API with <script setup>: better TypeScript inference, logic grouped by feature, and reuse through composables instead of mixins. The Options API is still supported and fine in existing code; mixing styles across a codebase is the main cost.

What a strong answer shows: they can name mixin problems (name collisions, unclear sources) that composables solve.

What are scoped slots and when would you use one?

A scoped slot lets the child pass data back to the parent's slot content, so the parent controls rendering while the child controls behavior. Typical uses: a data table that lets callers render cells, or a list with custom item templates.

What a strong answer shows: they can type slot props with defineSlots in a generic component.

Intermediate

Write a composable that fetches a user when an ID changes and cancels stale requests.

Accept a ref or getter, watch it, and register cleanup with onWatcherCleanup (Vue 3.5) before the first await, because cleanup can only be registered synchronously.

import { ref, watch, onWatcherCleanup, toValue, type MaybeRefOrGetter } from "vue"

export function useUser(id: MaybeRefOrGetter<string>) {
const user = ref<User | null>(null)
const error = ref<Error | null>(null)

watch(() => toValue(id), async (userId) => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort())
try {
const res = await fetch(`/api/users/${userId}`, { signal: controller.signal })
user.value = await res.json()
} catch (e) {
if (!controller.signal.aborted) error.value = e as Error
}
}, { immediate: true })

return { user, error }
}

What a strong answer shows: they handle the race where an old response arrives after a newer one.

How do you structure a Pinia store, and how should components read from it?

Setup stores look like composables: refs are state, computeds are getters, functions are actions. In components, use storeToRefs to destructure state and getters without losing reactivity; actions can be destructured directly.

export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([])
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
function add(item: CartItem) {
items.value.push(item)
}
return { items, total, add }
})

// in a component
const cart = useCartStore()
const { items, total } = storeToRefs(cart)
const { add } = cart

What a strong answer shows: they keep stores small and per-domain rather than one global store for everything.

How do you type provide and inject?

Export an InjectionKey<T> symbol from a shared module and use it on both sides, so inject returns the right type. Provide refs or readonly refs plus explicit update functions, rather than letting any descendant mutate shared state.

What a strong answer shows: they use provide/inject for component-tree context like forms or themes, not as a global store.

How do you access a DOM element or child component in Vue 3.5?

useTemplateRef matches the ref by string name, which also works when the ref name is dynamic.

<script setup lang="ts">
import { useTemplateRef, onMounted } from "vue"
const input = useTemplateRef<HTMLInputElement>("search")
onMounted(() => input.value?.focus())
</script>

<template><input ref="search" /></template>

What a strong answer shows: they know a child using <script setup> is closed by default and must use defineExpose to share methods.

How do you handle route-level data loading and access control with Vue Router?

Lazy-load route components with dynamic imports, use a global beforeEach guard for authentication with route meta for required roles, and redirect with the original path preserved. Load data in the component or a composable keyed on route params, and react to param changes when the same component is reused.

What a strong answer shows: they remember that client guards are UX only; the API still enforces authorization.

How do you test components and composables?

Vitest with Vue Test Utils for components: mount, interact through the DOM, assert on output and emitted events. Test composables directly, or inside a small host component when they rely on lifecycle hooks. For Pinia, createTestingPinia from @pinia/testing stubs actions and sets initial state.

What a strong answer shows: they test behavior users see instead of internal component state.

A component library needs a select component that works for any item type. How do you type it?

Use a generic SFC: <script setup lang="ts" generic="T">, with props like items: T[] and a typed model of T, plus defineSlots so option templates receive a typed item.

What a strong answer shows: they also cover keyboard navigation and ARIA roles, since a custom select replaces a native element.

Reactivity and rendering

A table with 10,000 rows from an API feels sluggish. What do you change?

Store data that is replaced but never mutated in place in shallowRef, so Vue doesn't create deep proxies for every row, and mark heavy third-party objects with markRaw. Render only what is visible with a virtual list, use stable keys, and consider v-memo for rows that rarely change.

What a strong answer shows: they profile with the Vue devtools and browser performance tools before optimizing.

A watcher on a settings object doesn't fire when a nested field changes. Why?

Watching a getter that returns the object only triggers when the object reference changes. Watch the specific nested getter, or pass deep: true; in Vue 3.5 deep also accepts a number to limit traversal depth. Watching a reactive object directly is deep by default.

What a strong answer shows: they prefer narrow getters over deep watchers on large objects.

Vue 3.5 made reactive props destructuring stable. What do you need to know?

Destructured props from defineProps stay reactive because the compiler rewrites access to props.x, and you can set defaults with JavaScript default values. Passing a destructured prop to watch or a composable still needs a getter.

const { count = 0, label } = defineProps<{ count?: number; label: string }>()

watch(() => count, (next) => console.log(label, next)) // works
// watch(count, ...) would pass a plain number and fail

What a strong answer shows: they understand it is compile-time sugar, not a runtime change.

How do you find out why a component re-renders too often?

Use the Vue devtools performance and component inspector, and in development the onRenderTracked and onRenderTriggered hooks to log which dependency triggered a render. Common causes: new object or function props created on every parent render, and a parent reading state it doesn't need.

What a strong answer shows: they move state down or split components rather than reaching for memoization first.

Memory grows the longer users keep the app open. Where do you look?

Listeners on window, intervals, observers and third-party instances (charts, maps) created in components and never cleaned up in onUnmounted. Watchers created asynchronously after an await aren't tied to the component and must be stopped manually. effectScope groups effects for disposal in composables used outside components.

What a strong answer shows: they use heap snapshots to confirm detached components before guessing.

When do you use KeepAlive and async components?

KeepAlive caches component instances between toggles, useful for tabs or list-detail navigation where state should survive; limit it with max and refresh data in onActivated. defineAsyncComponent splits heavy components into separate chunks, with loading and error states.

What a strong answer shows: they consider stale data and memory when caching instances.

Senior and architecture

In Nuxt, when do you use useFetch, useAsyncData and $fetch?

useFetch and useAsyncData fetch on the server during SSR and transfer the result in the payload, so the client doesn't fetch again during hydration. $fetch alone in setup would run twice, once on the server and again on the client. Use $fetch in event handlers and server routes.

const route = useRoute()
const { data: product, status, error } = await useFetch(
() => `/api/products/${route.params.id}`,
{ pick: ["id", "name", "price"] }
)

What a strong answer shows: they understand keys, payload size (hence pick) and how data is shared between components using the same key.

The console shows hydration mismatch warnings. What causes them and how do you fix them?

The server and client rendered different HTML: dates and time zones, random values or IDs, browser-only checks during render, or invalid HTML nesting that the browser rewrites. Render the same data on both sides, use useId for stable IDs, move browser-only output into onMounted or <ClientOnly>, and fix the markup.

What a strong answer shows: they also know Vue 3.5's lazy hydration strategies, such as hydrating a heavy component only when visible.

What changes when moving a Nuxt 3 app to Nuxt 4?

The main changes: application code moves into an app/ directory by default, useAsyncData and useFetch return shallow reactive data with undefined instead of null as the empty value, and components using the same key share data. Opting into the compatibility setting on Nuxt 3 first lets you test the new behavior incrementally.

What a strong answer shows: they would grep for deep mutations of fetched data and === null checks, which are the likely regressions.

How do you decide what state goes in Pinia, a server-state cache, local component state or the URL?

Server data with caching, refetching and invalidation needs fits a query library such as TanStack Query for Vue or Pinia Colada. Cross-cutting client state (session, cart, UI preferences) goes in Pinia. Filters, pagination and selected tabs belong in the URL so they survive reloads and sharing. Everything else stays local.

What a strong answer shows: they avoid copying server data into Pinia and hand-writing cache invalidation.

How would you migrate a large Vue 2 app to Vue 3?

Vue 2 reached end of life at the end of 2023, so this is a security issue as well as a modernization project. Upgrade to Vue 2.7 first, replace incompatible libraries and Vuex with Pinia where practical, then use the migration build (@vue/compat) to run on Vue 3 with warnings and fix them module by module. Remove filters, event buses and $listeners usage along the way.

What a strong answer shows: they plan around third-party dependencies, which usually block migrations more than the app's own code.

How do you configure rendering and caching per route in Nuxt?

Use routeRules: prerender marketing pages, use stale-while-revalidate or ISR for catalog pages, and render account pages on each request without caching. Keep secrets in runtimeConfig without the public prefix and call third-party APIs from server routes.

What a strong answer shows: they never cache pages that contain user-specific data at the edge.

A Vite build has grown slow and the main bundle is large. What do you do?

Analyze the bundle with a visualizer plugin, lazy-load routes and heavy components, replace large dependencies, and check that libraries are tree-shakable. For dev speed, look at plugin cost and dependency pre-bundling. Keep an eye on Vapor Mode, Vue's compile-to-DOM rendering mode arriving in the 3.6 line, for performance-critical components.

What a strong answer shows: they measure bundle composition before changing configuration.

Red flags to watch for

A practical exercise

Pair for 75 to 90 minutes on a small Nuxt or Vite app: a product search page with filters, results from a mock API and a detail drawer. Seed it with a filter state that isn't reflected in the URL, a watcher that fetches without cancelling stale requests, a destructured store that loses reactivity, and a hydration mismatch from a formatted date. Ask the candidate to fix the bugs, then add a "recently viewed" feature persisted across reloads.

Hire senior Vue.js developers vetted with these questions

Ryz introduces senior Vue.js developers who build with Vue 3, Pinia and Nuxt in production and can lead migrations off Vue 2. They are the top 1% of the candidates we interview, they work on your team and in your repos, and they work within ±1h of US time zones. See our vetting process or start with our Vue.js developer job description.

FAQ

How many Vue questions should I ask in one interview?

Five or six, chosen for the role, with follow-ups. Two deep reactivity discussions tell you more than ten quick definitions. Leave time for the candidate to write code.

Should I hire a React developer for a Vue role?

Strong React developers learn Vue quickly, since components, props and state map closely. Check that they understand Vue's proxy-based reactivity, which behaves differently from React's re-render model, and give them a week or two to adjust to Vue conventions.

Does a Vue developer need Nuxt experience?

Only if you render on the server. Nuxt adds SSR, data fetching, routing conventions and server routes, and its bugs (hydration, double fetching, caching) are specific. For a client-only app built with Vite, Vue, Pinia and Vue Router are what matter.

Questions we didn't answer? Email info@ryzlabs.com.

Explore Ryz Labs

Staff augmentationDedicated development teamsAI pod teamsForward deployed engineersNearshore software developmentAI engineering teamsHire engineers by roleRyz Labs vs competitorsAlternatives guidesBuyer guidesCase studiesHow we vet engineers
Ryz Labs

Senior engineers in your time zone. AI pod teams that ship.

Tell us who you need. You'll get a scoped plan, a price and the names of the people who would do the work.

Start a conversation →