How would you describe Vue.js and its primary purpose in web development?
Answer
Vue.js is a JavaScript framework for building UIs using components, declarative templates, reactive state, and directives.
These are the Vue questions you'll encounter in technical interviews, each with a detailed written answer. Start with reactivity and components, then work through the Composition API, routing, and state management.
Vue is a progressive JavaScript framework for building user interfaces. It pairs a reactive data model with a component system, and it scales from a single enhanced page to a full single-page application without forcing an all-or-nothing rewrite.
Vue interviews probe how reactivity actually works: refs versus reactive objects, computed properties, watchers, and the lifecycle of a component. You will also be asked to compare the Options API with the Composition API and to justify when each one fits.
Beyond the core, expect questions on component communication, slots, Vue Router, and state management with Pinia. Understanding how Vue tracks dependencies is what separates a confident answer from a memorised one.
Reading answers is not the same as giving them. Sit a full mock interview and get scored on how you actually explain them.
Try a mock interviewAnswer
Vue.js is a JavaScript framework for building UIs using components, declarative templates, reactive state, and directives.
Answer
Single-File Components (.vue) keep a component's template, script, and styles together, improving organization in build-tool projects.
Answer
The Composition API uses functions like ref(), reactive(), computed(), and watch() to organize component logic.
Answer
v-if unmounts elements from the DOM, reducing render cost initially. Use it when conditions change rarely.
Answer
Stable keys help Vue maintain component state across list changes. Index keys can cause state to persist on wrong rows.
Answer
Props pass data from parent to child. Children should emit events to request changes, not mutate props.
Answer
v-model on a component is a convention using a prop and update event, useful for form-like components.
Answer
Slots allow parents to pass template content to children. Useful for layout, cards, modals, and reusable shells.
Answer
Use provide/inject for shared context like theme or form state. Use props for direct parent-child communication.
Answer
Composables are functions combining Vue API features like ref, watch, and lifecycle hooks for reusable logic.
Answer
Vue.js is a JavaScript framework for building user interfaces using components, which are reusable, independent, and encapsulated pieces of UI. Each component has its own template (HTML), script (logic), and styles. Vue provides reactivity through a dependency-tracking system that updates components when state changes. The framework supports progressive adoption, allowing use on part of a page or as a full application. Key features include declarative templates, directives, component lifecycle, and an ecosystem for routing, state management, and server rendering.
Answer
Single-File Components (.vue files) contain a component's template, script, and styles in one file. The structure separates concerns into three sections: template for markup, script for logic, and optional style for scoped CSS. Benefits include keeping related code together, improving maintainability, enabling scoped styling to prevent conflicts, supporting modern tooling like TypeScript and preprocessors, and making components portable and reusable. SFCs are the standard approach in modern Vue projects with build steps.
Answer
The Options API organizes logic into options like data, methods, computed, watch, and lifecycle hooks. It's approachable for simpler components and familiar to developers from other frameworks. The Composition API uses imported functions like ref, reactive, computed, and watch to write logic in any order. It scales better for complex components, provides better code organization for related logic, and enables extraction into reusable composables. Neither is universally better; Options API suits straightforward components while Composition API excels with shared logic and complexity.
Answer
ref() creates a reactive wrapper around a value. In templates, refs are automatically unwrapped; in scripts, access the value via .value property. Use ref() for primitive values (strings, numbers, booleans) and when a value might be replaced entirely. Unlike reactive(), which wraps objects, ref() works for any type and supports complete reassignment. Example: const count = ref(0); count.value += 1 updates the reactive value.
Answer
reactive() creates a reactive proxy for objects. Modifications to object properties trigger reactivity. The pitfall occurs when destructuring because destructuring breaks reactivity: const { email } = form gives you the current value but not a reactive binding. To preserve reactivity when destructuring, use toRefs(): const { email } = toRefs(form) creates reactive bindings. This is a common source of 'why isn't my template updating?' bugs.
Answer
computed() creates derived values calculated from reactive state, used during render. It's ideal for formatting, filtering, or transforming data. computed() is cached and only recalculates when dependencies change. watch() handles side effects when reactive values change, like API calls, storage sync, logging, or bridging to non-Vue code. Use computed when you can calculate during render; use watch for side effects that shouldn't run every render. If you can calculate it, use computed.
Answer
Vue tracks reactive reads during rendering and effect execution, recording which values were accessed. When tracked reactive values change, Vue updates the affected parts without rerendering everything. Vue 3 uses JavaScript proxies for object reactivity and ref wrappers for individual values. The system automatically establishes dependencies without manual declaration. This fine-grained reactivity means only components that depend on changed data update, not the entire app.
Answer
Composables are functions combining Composition API features to package reusable stateful logic. They use ref, reactive, computed, watch, and lifecycle hooks to create self-contained, portable units of functionality. Good composables clean up side effects in onUnmounted, have small APIs, and handle their own state. Example: useWindowWidth() manages window resize listeners and exposes a reactive width value. Composables replace mixins for better code reuse with clearer dependencies.
Answer
Separate request state into distinct properties: loading (boolean), data (result when successful), error (error message), and empty (no results). Fetch data when routes load or inputs change. Model the flow: initial loading state, transition to success/error/empty based on result. Always show one state to users: loading spinner, data, error message with retry option, or empty state message. In SSR or framework setups, prefer the framework's data-loading pattern over component-level fetching.
Answer
Put shareable, bookmarkable state in the URL: search queries, filters, sort order, page numbers, selected tabs where back-button behavior is expected. Keep ephemeral UI state local: open menus, draft form input, hover state, temporary loading flags. This distinction lets users bookmark and share URLs representing the exact view they see. Local state avoids polluting the URL bar and stays easier to reason about since only the component owns it.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I'd use props for the open state, emits for close and confirm events, and slots for content. For accessibility, I'd add focus trapping using onMounted and onUnmounted lifecycle hooks to manage focus on open/close. I'd handle Escape key dismissal, lock scroll when modal opens, and set proper ARIA attributes like role='dialog' and aria-label. I'd test keyboard navigation to ensure it's fully accessible. The component should clean up all event listeners in onUnmounted to prevent memory leaks.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I used provide/inject when building a form with many nested field components. Passing props through every level created prop drilling that made changes difficult. By providing form state and validation functions at the top, child fields accessed what they needed without intermediate components caring. It was cleaner and changes to form logic didn't ripple through the tree. Props would still be used for direct parent-child communication; provide/inject solved the deep nesting problem.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I'd first check if the value is actually reactive by adding a console.log and verifying it changes. Then I'd check for common reactivity breaking patterns: destructuring without toRefs, reassigning ref.value outside reactive scope, or mutating nested object properties. I might be using v-show when I meant v-if, or the dependency might not be declared in computed or watch. I'd also verify the component is actually bound to the data, not showing stale values in the template.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I profiled the component and found rendering thousands of rows in a list was the bottleneck. I implemented virtual scrolling to render only visible rows, reducing DOM nodes dramatically. I also moved expensive calculations to computed properties instead of template expressions. I added stable keys to prevent state loss when filtering. For large immutable datasets, I used shallowRef to avoid deep reactivity overhead. These changes made the list interactive even with large datasets.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I organize by feature when features are substantial: each feature gets a folder with route, components, composables, store (if needed), and API client. Shared UI lives in a shared components folder. Utilities and helpers go in a common folder. This structure scales; features can be developed independently. I avoid a monolithic components folder with hundreds of one-offs. Types go alongside code that uses them. Each section is clear about what it owns.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Discuss your approach and the tradeoffs you considered
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Discuss your approach and the tradeoffs you considered
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Discuss your approach and the tradeoffs you considered
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Discuss your approach and the tradeoffs you considered
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Discuss your approach and the tradeoffs you considered
<template>
<div class="counter">
<!-- Display counter value and add buttons -->
</div>
</template>
<script setup lang="ts">
// Implement counter logic here
</script>Answer
<template>
<div class="counter">
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
</script><template>
<div class="products">
<input v-model="searchTerm" placeholder="Search products..." />
<!-- Render filtered products -->
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const searchTerm = ref('');
const products = [
{ id: 1, name: 'Laptop' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Keyboard' }
];
// Add filtering logic here
</script>Answer
<template>
<div class="products">
<input v-model="searchTerm" placeholder="Search products..." />
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const searchTerm = ref('');
const products = [
{ id: 1, name: 'Laptop' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Keyboard' }
];
const filteredProducts = computed(() =>
products.filter(p =>
p.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
</script><template>
<div class="details">
<button @click="toggleDetails">Show Details</button>
<!-- Conditionally render details -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// Add logic here
</script>Answer
<template>
<div class="details">
<button @click="toggleDetails">
{{ showDetails ? 'Hide Details' : 'Show Details' }}
</button>
<div v-if="showDetails" class="content">
<p>This is additional information about the item.</p>
<p>It appears and disappears from the DOM.</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const showDetails = ref(false);
function toggleDetails() {
showDetails.value = !showDetails.value;
}
</script><template>
<div class="dropdown">
<button @click="toggleMenu">{{ selectedOption || 'Select an option' }}</button>
<!-- Render menu items when open -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const isOpen = ref(false);
const selectedOption = ref(null);
const options = ['Option A', 'Option B', 'Option C'];
const emit = defineEmits<{
select: [option: string];
}>();
// Add logic here
</script>Answer
<template>
<div class="dropdown">
<button @click="toggleMenu">{{ selectedOption || 'Select an option' }}</button>
<ul v-if="isOpen" class="menu">
<li v-for="option in options" :key="option" @click="selectOption(option)">
{{ option }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const isOpen = ref(false);
const selectedOption = ref(null);
const options = ['Option A', 'Option B', 'Option C'];
const emit = defineEmits<{
select: [option: string];
}>();
function toggleMenu() {
isOpen.value = !isOpen.value;
}
function selectOption(option: string) {
selectedOption.value = option;
isOpen.value = false;
emit('select', option);
}
</script><template>
<div class="shopping">
<ul>
<!-- Display items with prices -->
</ul>
<p>Total: {{ total }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const items = ref([
{ name: 'Book', price: 15 },
{ name: 'Pen', price: 2 },
{ name: 'Notebook', price: 8 }
]);
// Calculate total with computed
</script>Answer
<template>
<div class="shopping">
<ul>
<li v-for="item in items" :key="item.name">
{{ item.name }}: ${{ item.price }}
</li>
</ul>
<p>Total: ${{ total }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const items = ref([
{ name: 'Book', price: 15 },
{ name: 'Pen', price: 2 },
{ name: 'Notebook', price: 8 }
]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
);
</script><template>
<div class="email-form">
<input v-model="email" type="email" placeholder="Enter email" />
<!-- Show validation error if needed -->
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const email = ref('');
// Add validation logic
</script>Answer
<template>
<div class="email-form">
<input v-model="email" type="email" placeholder="Enter email" />
<p v-if="emailError" class="error">{{ emailError }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const email = ref('');
const emailError = computed(() => {
if (!email.value) return '';
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email.value) ? '' : 'Invalid email format';
});
</script><template>
<div>
<button @click="toggleActive">Toggle Style</button>
<!-- Apply active class conditionally -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// Add logic here
</script>
<style scoped>
.active {
background-color: green;
color: white;
}
</style>Answer
<template>
<div>
<button @click="toggleActive">Toggle Style</button>
<div :class="{ active: isActive }" class="box">
Styled Content
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const isActive = ref(false);
function toggleActive() {
isActive.value = !isActive.value;
}
</script>
<style scoped>
.box {
padding: 20px;
background-color: gray;
}
.active {
background-color: green;
color: white;
}
</style><template>
<div class="todos">
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a todo" />
<!-- Display todos with completion toggle -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// Add todo logic here
</script>Answer
<template>
<div class="todos">
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a todo" />
<ul>
<li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.done }">
<input type="checkbox" v-model="todo.done" />
{{ todo.text }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const newTodo = ref('');
const todos = ref([]);
let todoId = 0;
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({ id: ++todoId, text: newTodo.value, done: false });
newTodo.value = '';
}
}
</script><template>
<div class="inventory">
<ul>
<!-- Display products with formatted prices -->
</ul>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
const products = ref([
{ id: 1, name: 'Widget', price: 19.99 },
{ id: 2, name: 'Gadget', price: 49.50 }
]);
// Add formatting logic
</script>Answer
<template>
<div class="inventory">
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }}: {{ formattedPrice(product.price) }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const products = ref([
{ id: 1, name: 'Widget', price: 19.99 },
{ id: 2, name: 'Gadget', price: 49.50 }
]);
function formattedPrice(price: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
}
</script><template>
<button @click="handleClick">
Click Me
</button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
myEvent: [message: string];
}>();
function handleClick() {
// Emit event with data
}
</script>Answer
<template>
<button @click="handleClick">
Click Me
</button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
myEvent: [message: string];
}>();
function handleClick() {
emit('myEvent', 'Button was clicked!');
}
</script>