Top Vue.js Interview Questions & Answers

Vue.js interviews focus on its reactivity system and component composition patterns, and how they differ between the Options API and Composition API.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →

1. Explain Vue's reactivity system. How does it actually work under the hood, and what changed between Vue 2 and Vue 3?

Vue's reactivity system is what lets your UI automatically update when the underlying data changes, without you manually calling something like `setState`. In Vue 2, this was implemented by walking through every property on a data object and converting it into a getter/setter pair using `Object.defineProperty`, so Vue could detect reads and writes. Vue 3 rewrote this using JavaScript `Proxy` objects instead, which wrap the entire object rather than individual properties. Under the hood, when a component renders, it runs inside a tracked effect that records which reactive properties it read; when one of those properties later changes, the effect re-runs and the component re-renders. The core pieces are `track()`, which records dependencies during a get, and `trigger()`, which notifies those dependencies during a set.

2. Why did Vue 3 move to a Proxy-based reactivity system instead of Object.defineProperty?

`Object.defineProperty` has a few hard limitations that Vue 2 developers ran into constantly. It can only intercept access to properties that already exist on an object at the time it's made reactive, so adding a new property later with `this.foo = 'bar'` wouldn't be reactive unless you used `Vue.set`. It also couldn't detect array index assignments or length changes, which is why Vue 2 had to patch array mutator methods like `push` and `splice` to make them reactive. A `Proxy`, on the other hand, wraps the whole object and intercepts operations like `get`, `set`, `has`, and `deleteProperty` generically, so new properties, deleted properties, and array indices are all reactive automatically. This also means conversion can happen lazily as properties are accessed, instead of Vue having to recursively walk and convert every nested property upfront.

3. What's the difference between the Options API and the Composition API?

The Options API is Vue's original style, where you organize a component by option type, things like `data`, `methods`, `computed`, and `watch`, and Vue merges them all together at runtime. The Composition API, introduced in Vue 3 via the `setup()` function or `<script setup>`, lets you organize code by logical concern instead, so all the state, computed values, and functions for one feature can live together even if they'd otherwise be scattered across different option types. This makes it much easier to extract and reuse logic across components through composables, which are just plain functions that use reactivity primitives like `ref` and `computed`. The Options API can start to struggle in large components because related logic for one feature ends up spread across the file's data, methods, and watch sections. Both APIs compile down to the same underlying reactivity system, so it's not a difference in capability, and you can technically mix them in one component, though most modern codebases default to the Composition API for anything nontrivial.

4. When would you use a computed property versus a watcher?

A computed property is for deriving a new value from existing reactive state, it's declarative, cached based on its dependencies, and only recalculates when one of those dependencies actually changes. A watcher, by contrast, is for running a side effect in response to a change, like making an API call, manipulating a DOM element directly, or kicking off another async update. The rule of thumb is that if you're computing a value to display or reuse elsewhere, reach for `computed`, and if you're reacting to a change to do something imperative, use `watch` or `watchEffect`. Computed properties are lazy and cached, so accessing the same computed multiple times between dependency changes won't rerun the getter, whereas a watcher fires once for every relevant change regardless of whether you read its result. Trying to force a watcher to do a computed's job usually just adds extra state and boilerplate that a computed property would have handled for free.

5. What's the difference between ref and reactive in the Composition API?

`ref` wraps a value, primitive or object, in an object with a single `.value` property, and Vue tracks reads and writes to that `.value`. `reactive`, on the other hand, wraps an object directly in a `Proxy` and makes all of its properties reactive without needing a `.value` accessor. The reason `ref` exists at all is that primitives like numbers and strings can't be proxied, since a `Proxy` needs an actual object to wrap. In templates, Vue automatically unwraps `ref`s so you write `count` instead of `count.value`, but inside `<script>` you always need the `.value`. A common gotcha is that destructuring a `reactive` object breaks reactivity because you lose the Proxy wrapper, whereas `ref`s survive destructuring, especially when combined with a helper like `toRefs`.

6. How do components communicate with each other in Vue?

The default pattern is described as "props down, events up": a parent passes data to a child through props, and the child notifies the parent of changes by emitting custom events with `$emit`, or `emit()` in the Composition API. This keeps data flow one-directional and predictable, since a child should never directly mutate a prop it received. For communication between components that aren't in a direct parent-child relationship, you'd typically reach for `provide`/`inject` for deeply nested trees, or a shared store like Pinia for state that many unrelated components need. Vue also supports template refs, which let a parent directly call a method exposed on a child instance, though this is used sparingly since it breaks the declarative data-flow model. Slots are another form of parent-to-child communication, letting a parent inject markup into a child's layout rather than just plain data.

7. What is provide/inject, and when would you use it over props?

`provide` and `inject` let an ancestor component expose a value that any descendant can consume, no matter how deeply nested, without threading it through every intermediate component as props. A parent, or any ancestor, calls `provide('key', value)`, and any component below it in the tree can call `inject('key')` to receive that value. This solves the "prop drilling" problem, where you'd otherwise have to pass a prop through several layers of components that don't actually use it themselves, just to get it down to a deeply nested child. It's commonly used for things like theming, localization, or exposing a form's shared state to arbitrary nested field components. Unlike props, the relationship is implicit rather than visible in the component's interface, so it's best reserved for cross-cutting concerns rather than used as a general replacement for props, since overusing it makes data flow harder to trace.

8. What's the difference between v-if and v-show?

`v-if` conditionally renders an element by actually adding or removing it from the DOM along with its component instance, meaning lifecycle hooks fire and local state resets each time it toggles back on. `v-show` always keeps the element in the DOM and just toggles its CSS `display` property between `none` and its normal value, so the component instance and its state persist across toggles. Because of this, `v-if` has a higher toggle cost since it involves creating and destroying DOM nodes and component instances, while `v-show` has a higher upfront render cost since the element is rendered regardless of the initial condition. The general guidance is to use `v-show` for elements that toggle frequently, like a dropdown or tab panel, and `v-if` for things that rarely change or where you actually want the element's state to reset. `v-if` also supports `v-else` and `v-else-if` chains, whereas `v-show` has no equivalent since it's purely a display toggle.

9. Why does Vue require a key when using v-for?

When Vue renders a list with `v-for`, it needs an efficient way to figure out which DOM nodes to reuse, move, or remove when the underlying array changes, and the `key` attribute gives it a stable identity for each item to track that with. Without a `key`, Vue falls back to an in-place patch strategy that reuses DOM nodes purely by position, which can cause bugs where stateful elements, like an input's typed value or a component's local state, end up attached to the wrong data after a reorder. By providing a unique and stable `key`, usually an item's id rather than its array index, Vue can correctly match which node corresponds to which piece of data even after the array is sorted, filtered, or has items inserted in the middle. Using the array index as a key is a common anti-pattern precisely because the index changes whenever items are added or removed, which defeats the whole point of having a stable identity. It's essentially the same list-reconciliation problem React's `key` prop solves, just implemented in Vue's own diffing algorithm.

10. What is a single-file component (.vue file), and what are its main sections?

A single-file component is a `.vue` file that bundles a component's template, script, and styles together in one file, instead of spreading them across separate HTML, JS, and CSS files. It has three main blocks: a `<template>` section containing the HTML-like markup, a `<script>` or `<script setup>` section containing the component's logic, and an optional `<style>` section for CSS, which can be scoped to just that component using the `scoped` attribute. SFCs aren't valid JavaScript or HTML on their own; a build tool like Vite or webpack, using `@vitejs/plugin-vue` or `vue-loader`, compiles the file and turns the template into a render function. This colocation makes components easier to reason about and move around as a unit, since everything relevant to that component lives in one place. Scoped styles work by adding a unique data attribute to the component's rendered elements at compile time, so the CSS rules only ever match elements within that component.

11. Walk through Vue's component lifecycle hooks.

Every Vue component goes through a lifecycle from creation to destruction, and Vue exposes hooks at each stage so you can run code at the right time. `beforeCreate` and `created` fire before and after the instance is initialized with reactive data, though in the Composition API that setup logic just lives directly in `setup()` since there's no real equivalent needed for those two. `beforeMount` and `mounted` fire before and after the component is inserted into the DOM, and `mounted` is typically where you'd fetch data, measure a DOM element, or initialize a third-party library that needs a real DOM node to attach to. `beforeUpdate` and `updated` fire around re-renders caused by reactive data changes, and `beforeUnmount` and `unmounted` fire before and after the component is removed, which is where you clean up event listeners, timers, or subscriptions. In the Composition API, these are accessed as functions like `onMounted`, `onUpdated`, and `onUnmounted`, imported from `vue` and called inside `setup()`.

12. What's the difference between Vuex and Pinia?

Vuex was Vue's original official state management library, structured around a single store with `state`, `getters`, `mutations`, and `actions`, where mutations were the only way to synchronously change state and actions handled async logic before committing a mutation. Pinia is the newer, now-official state management library that simplifies this considerably: it drops mutations entirely, so you just call actions or modify state directly, and stores are defined as plain functions using either an Options-like syntax or a Composition-API-style setup function. Pinia also has first-class TypeScript support, whereas Vuex's typing always felt bolted on, and Pinia stores are naturally tree-shakeable and modular since each store is its own independent unit rather than a nested module tree. Devtools support is comparable between the two, but Pinia's API surface is smaller and more intuitive, closer to how you're already thinking when writing Composition API code. Because of this, the Vue team recommends Pinia for new projects, and Vuex is now considered to be in maintenance mode.

13. What is the virtual DOM, and how does Vue use it?

The virtual DOM is a lightweight JavaScript representation of the actual DOM tree, made up of plain objects describing what elements, attributes, and children should exist. When a component's state changes, Vue doesn't touch the real DOM directly; it re-runs the component's render function to produce a new virtual DOM tree, then diffs it against the previous one to figure out the minimal set of actual changes needed. This diffing process, often called reconciliation, lets Vue batch and minimize expensive real DOM operations, since comparing JavaScript objects is far cheaper than manipulating the actual DOM. Vue 3 also added compiler optimizations on top of this, like marking static content at compile time so it's skipped entirely during diffing, and flagging which bindings are dynamic so the diff only checks parts of the tree that can actually change. The end result is that developers write declarative templates without having to manually track which DOM nodes need updating.

14. Explain the different types of slots in Vue: default, named, and scoped.

Slots are Vue's mechanism for letting a parent component inject content into specific placeholders within a child component's template, similar to `children` in React but more flexible. A default slot is the simplest form: you write `<slot></slot>` in the child, and whatever markup the parent places between the child's opening and closing tags gets rendered there. Named slots let a component expose multiple insertion points by giving each `<slot>` a `name` attribute, and the parent targets a specific one using `<template v-slot:name>` or the shorthand `#name`. Scoped slots go a step further by letting the child pass data back up to the parent's slot content, using `<slot :item="item">` in the child and `<template #default="{ item }">` in the parent, which is how a reusable table or list component can let the parent control rendering while the child still owns the underlying data. This combination makes slots one of the most powerful tools for building genuinely reusable, flexible components in Vue.

15. How does Vue Router handle navigation, and what are navigation guards?

Vue Router is the official routing library for Vue, and it works by mapping URL paths to components, so navigating to a route swaps out whatever's currently rendered inside a `<router-view>` element. Routes are defined as an array of objects with a `path` and a `component`, and you navigate between them either declaratively with `<router-link to="/path">` or programmatically with `router.push('/path')`. Navigation guards let you hook into the routing lifecycle to control access or trigger side effects: `beforeEach` is a global guard that runs before every navigation, `beforeEnter` is defined per-route, and `beforeRouteEnter`, `beforeRouteUpdate`, and `beforeRouteLeave` are defined inside a component itself. A common use case for guards is authentication, where a `beforeEach` guard checks whether a route requires login and redirects to a login page if the user isn't authenticated. Vue Router also supports nested routes, dynamic segments like `/users/:id`, and lazy-loaded route components for code splitting.

16. How does v-model work under the hood?

`v-model` is syntactic sugar for two-way binding on form inputs and components, and under the hood it expands into a prop plus an event listener. On a native input, `<input v-model="text">` compiles roughly to binding `:value="text"` and listening for the `input` event to update `text` with the new value, and Vue adjusts this slightly for checkboxes, radios, and selects to use the appropriate prop and event pairing. On a custom component, `v-model` by default expands to a `modelValue` prop and an `update:modelValue` event, so the child needs to accept that prop and emit that event whenever its internal value changes. Vue 3 also allows multiple `v-model` bindings on the same component through named arguments, like `v-model:title="title"`, which maps to a `title` prop and an `update:title` event. This makes it straightforward to build custom form components, like a styled input or a custom dropdown, that feel just as native as built-in form elements when used with `v-model`.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →
Related topics
ReactAngularNext.jsRedux