\n```\n\nThe consumer then supplies its own markup while reusing all the internal logic:\n\n```html\n\n \n
\n {{ col.label }} ↓\n
\n \n \n
{{ row.name }}
{{ row.email }}
\n \n\n```\n\nThe key architectural point: the DataTable never assumes it's rendering a `
` at all — it just hands out data and callbacks and trusts the slot content to render however the consumer wants."}},{"@type":"Question","name":"Implement a reusable click-outside custom directive in Vue 3 that closes dropdown menus. How do you implement mounted and unmounted lifecycle hooks to attach and cleanly detach event listeners on document, ensuring no memory leaks occur when components are destroyed?","acceptedAnswer":{"@type":"Answer","text":"A custom directive is an object with lifecycle hooks that mirror component lifecycle but operate on a raw DOM element instead of a component instance. For click-outside, `mounted` is where you attach a document-level click listener, and `unmounted` is where you must remove that exact same listener — the memory-leak risk here is that if you don't keep a reference to the handler function you attached, you can't remove it later, and it keeps firing (and keeping the element/component alive in memory) forever after the component is gone.\n\n```js\nconst clickOutside = {\n mounted(el, binding) {\n el._clickOutsideHandler = (event) => {\n if (!el.contains(event.target)) {\n binding.value(event) // call the handler passed in via v-click-outside=\"closeDropdown\"\n }\n }\n document.addEventListener('click', el._clickOutsideHandler)\n },\n unmounted(el) {\n document.removeEventListener('click', el._clickOutsideHandler)\n delete el._clickOutsideHandler\n },\n}\n```\n\nUsage:\n\n```html\n
...
\n```\n\nStoring the handler reference on `el` itself (rather than a closure variable outside the directive) is what makes `removeEventListener` actually work — `removeEventListener` requires the exact same function reference used in `addEventListener`, so if `mounted` created a fresh anonymous function each time without saving it somewhere accessible in `unmounted`, the removal call would silently fail to remove anything, and the listener would keep firing against a detached element and its closed-over scope indefinitely."}},{"@type":"Question","name":"During SSR in Nuxt 3 / Vue SSR, accessing window.innerWidth or generating random UUIDs inside a script setup block causes server-rendered HTML to mismatch client DOM, throwing hydration mismatch errors. How do you diagnose and resolve hydration errors using ClientOnly, onMounted, and deterministic SSR state hydration?","acceptedAnswer":{"@type":"Answer","text":"SSR hydration works on the assumption that the HTML the server rendered is exactly what the client would render too, on the very first render pass — Vue then just 'attaches' its reactivity to that existing markup instead of throwing it away and re-rendering. `window` doesn't exist on the server at all, so code referencing `window.innerWidth` at setup-time either crashes the server render outright or (if guarded) produces a different value than the client would compute, and something like `crypto.randomUUID()` called at setup-time literally produces a different value on the server versus the client's own first render — either way, the two HTML outputs disagree, and Vue's hydration mismatch warning is exactly it noticing that disagreement.\n\nThe fix is to make sure anything browser-only or non-deterministic never runs during the render that produces the initial HTML — only after hydration is complete. `onMounted` only fires client-side, after the component has already mounted onto (or hydrated into) real DOM, so it's a safe place for window access or generating client-only values:\n\n```js\nconst width = ref(0)\nonMounted(() => {\n width.value = window.innerWidth // never touched during SSR, so nothing to mismatch\n})\n```\n\nFor a whole sub-tree that's fundamentally impossible to render consistently on the server (e.g. a chart relying entirely on canvas/window sizing), Nuxt's `` wrapper skips SSR for that block entirely and renders a fallback (or nothing) until the client mounts it for real — no mismatch possible because the server never attempted to render it in the first place. For state that genuinely needs to be identical between server and client (like a generated id used for accessibility attributes), generate it deterministically instead — derive it from stable props/data rather than randomness, or in Vue 3.5+ use the built-in `useId()` composable, which is specifically designed to produce SSR-safe, hydration-consistent ids."}},{"@type":"Question","name":"You are hosting multiple Vue 3 micro-apps on a single page shell. How do you prevent collisions across distinct Vue Router instances and ensure each micro-app encapsulates its own Pinia store instance without leaking state into the host container?","acceptedAnswer":{"@type":"Answer","text":"The core discipline in a micro-frontend setup is: every micro-app calls `createApp()`, `createPinia()`, and `createRouter()` for itself, and none of those instances are shared globally across micro-apps or with the host shell. Pinia and Vue Router both attach themselves to the specific app instance they're installed via `app.use(...)` on — as long as each micro-app builds and installs its own instances rather than reusing a singleton exported from somewhere shared, there's no state collision possible, because each Pinia instance's store registry is scoped to that one Pinia instance.\n\n```js\n// inside each micro-app's own bootstrap file — never imported/shared across micro-apps\nconst app = createApp(MicroAppRoot)\nconst pinia = createPinia()\nconst router = createRouter({ history: createWebHistory('/micro-app-a/'), routes })\n\napp.use(pinia)\napp.use(router)\napp.mount('#micro-app-a-container') // mounts into its OWN container, not the host's #app\n```\n\nFor router collisions specifically, two separate `createRouter()` instances each attach their own popstate/history listeners — if both try to control the same browser history and both use a base path that overlaps (or both try to claim `/`), they'll fight over navigation events. Giving each router its own distinct base path segment (`/micro-app-a/`, `/micro-app-b/`) and mounting each micro-app into its own DOM container rather than a shared root element keeps their routing, event listeners, and reactive stores fully isolated — the host shell just orchestrates which container is visible, without either micro-app needing to know the other exists."}},{"@type":"Question","name":"What is the difference between v-if and v-show, and when would you choose one over the other?","acceptedAnswer":{"@type":"Answer","text":"v-if actually adds or removes the element (and its component, with full mount/unmount lifecycle) from the DOM based on the condition. v-show always keeps the element in the DOM and just toggles its CSS `display` property between `none` and its normal value.\n\nBecause v-if triggers real mount/unmount, it's more expensive to toggle repeatedly but cheaper when the condition rarely changes (you don't pay for rendering something the user may never see). v-show is the opposite: cheap to toggle since it's just a style flip, but the element is always rendered underneath, so it costs more upfront and isn't appropriate for content that shouldn't even exist until a condition is met (e.g. content gated behind a permission check — v-show would still put it in the DOM, just hidden, which a curious user could reveal via devtools).\n\n```html\n\n\n\n\n
...
\n```\n\nRule of thumb: v-if for conditional existence (and especially for anything sensitive or expensive to mount), v-show for frequent visual toggling of content that's fine to keep rendered."}},{"@type":"Question","name":"Why does Vue require a unique :key when rendering a list with v-for, and what goes wrong if you use the array index as the key?","acceptedAnswer":{"@type":"Answer","text":"The key gives Vue a stable identity for each item across re-renders, so its diffing algorithm can tell 'this is the same item, just moved/updated' apart from 'this is a genuinely new item.' Without a reliable key, Vue falls back to matching elements by position, which works fine for simple appends but breaks down the moment the list is reordered, filtered, or has items inserted/removed from the middle.\n\nUsing the array index as the key seems convenient but reintroduces exactly that problem: if you delete the second item in a 5-item list, every item after it shifts up one index, so Vue thinks 'index 2 changed from item C to item D' rather than 'item C was removed' — even though nothing about item C's content actually changed, Vue may reuse and patch DOM nodes incorrectly. This gets especially nasty with list items that hold their own local state (like an input field or a component with internal reactive data) — that state can end up attached to the wrong visual row after a reorder, because Vue matched by position, not by real identity.\n\n```html\n\n
...
\n\n\n
...
\n```\n\nIndex-as-key is only safe for lists that are static or purely append-only and never reorder/filter/splice."}},{"@type":"Question","name":"What's the difference between a computed property and a watcher, and how do you decide which to use?","acceptedAnswer":{"@type":"Answer","text":"A computed property describes a value that's derived from other reactive state — you declare what it IS, and Vue figures out when to recalculate it, caching the result until a dependency actually changes. A watcher describes a reaction to a change — you're saying 'when this specific thing changes, go do this side effect,' and it doesn't return a value at all; its job is to perform an action (an API call, a DOM manipulation, updating unrelated state).\n\nThe practical test: if you're computing a new value FROM existing reactive data and just want to display or reuse that value, use computed — it's cached, declarative, and can be used directly in a template like a property. If you need to DO something in response to a change — fetch data, log an event, sync to localStorage, trigger an animation — use watch or watchEffect.\n\n```js\n// computed: deriving a value\nconst fullName = computed(() => `${firstName.value} ${lastName.value}`)\n\n// watch: reacting with a side effect\nwatch(userId, async (newId) => {\n profile.value = await fetchProfile(newId)\n})\n```\n\nA common beginner mistake is trying to use a watcher to just mirror one ref into another derived ref — that's what computed is for, more concisely and with automatic caching."}},{"@type":"Question","name":"What are the main lifecycle hooks available in the Vue 3 Composition API, and what would you typically use each for?","acceptedAnswer":{"@type":"Answer","text":"The Composition API exposes lifecycle hooks as functions you call inside setup() (or `\n```\n\nThat last one, `defineExpose`, matters because `\n\n \n\n```\n\n```html\n\n\n\n\n```\n\nYou can also support multiple, named v-model bindings on one component by using an argument, `v-model:title=\"docTitle\"`, which maps to a `title` prop and an `update:title` event instead of the default `modelValue`/`update:modelValue` pair — useful for a component that needs two-way binding on more than one value at once (e.g. both a date and a time on a scheduling widget)."}},{"@type":"Question","name":"What is Teleport in Vue 3 and what problem does it solve?","acceptedAnswer":{"@type":"Answer","text":"Teleport lets you render a piece of a component's template into a completely different location in the actual DOM, while the component that owns that content stays logically in its normal place in the component tree (still has access to its parent's data, props, and reactive state — Teleport only moves where the DOM nodes physically end up, not the component's logical position).\n\nThe problem it solves is CSS stacking-context and overflow issues for things like modals, tooltips, and dropdowns. If a modal is rendered deep inside a component that has `overflow: hidden` or a `z-index`/`transform` on an ancestor, the modal can get visually clipped or trapped behind other content no matter how high you set its own z-index — because it's a CSS descendant of that constrained ancestor. Teleporting the modal's actual DOM output to `document.body` sidesteps the problem entirely, since it's no longer a DOM descendant of the constrained container.\n\n```html\n\n \n \n
\n
Modal content, unaffected by any ancestor's overflow/z-index
\n
\n \n\n```\n\nThe component's own state (`open`) still drives the teleported content exactly as if it hadn't moved — Teleport is purely a DOM placement mechanism, not a change to the reactive/component ownership graph."}},{"@type":"Question","name":"How do Vue Router navigation guards differ — specifically global beforeEach, per-route beforeEnter, and in-component beforeRouteLeave?","acceptedAnswer":{"@type":"Answer","text":"Global guards, per-route guards, and in-component guards all run at different points and scopes in the navigation resolution pipeline, which matters for where you put a given check. `router.beforeEach` registers a guard on the router instance itself, and it runs for every single navigation, anywhere in the app — the natural place for app-wide checks like 'is the user authenticated' or global analytics tracking.\n\n`beforeEnter`, defined in the route config for a specific route, only runs when navigating TO that particular route — useful for route-specific checks (like 'does this user have permission for /admin') without cluttering the global guard with route-specific conditionals.\n\n`beforeRouteLeave`, defined inside the component being navigated away from (or via `onBeforeRouteLeave` in Composition API), runs when the user is about to leave that specific component's route — the natural place for 'you have unsaved changes, are you sure?' confirmations, since it has access to the component's own local state.\n\n```js\n// global — runs on every navigation\nrouter.beforeEach((to, from) => {\n if (to.meta.requiresAuth && !isLoggedIn()) return '/login'\n})\n\n// per-route — only for this route\n{ path: '/admin', component: Admin, beforeEnter: (to) => hasAdminRole() || '/403' }\n\n// in-component — only when leaving THIS component's route\nonBeforeRouteLeave((to, from) => {\n if (hasUnsavedChanges.value) return confirm('Discard changes?')\n})\n```\n\nAll guard types resolve in a defined order (global beforeEach → route beforeEnter → in-component beforeRouteLeave for the outgoing component, then more global/route hooks) before the navigation actually completes, and any of them can cancel or redirect the navigation by returning `false` or a path."}},{"@type":"Question","name":"How do you implement lazy-loaded (code-split) routes in Vue Router, and why does it matter for initial load performance?","acceptedAnswer":{"@type":"Answer","text":"Instead of statically importing every route's component at the top of your router config — which bundles all of them into one big initial JS file the browser has to download before the app can even start — you use a dynamic `import()` as the route's component definition. The build tool (Vite/webpack) automatically splits each dynamically-imported component into its own separate chunk, which only gets downloaded when the user actually navigates to that route.\n\n```js\nconst routes = [\n { path: '/', component: Home }, // eager: needed immediately\n { path: '/admin', component: () => import('./views/Admin.vue') }, // lazy: own chunk, loaded on demand\n { path: '/reports', component: () => import('./views/Reports.vue') },\n]\n```\n\nThis matters a lot for initial load time on larger apps — a user landing on the homepage shouldn't have to download the code for the admin panel, the reports page, and every other route they may never visit in that session. The tradeoff is a small delay the first time a user navigates to a lazy route (the chunk has to download), which you can soften with a loading indicator or by prefetching likely-next routes. Grouping related routes into the same chunk (via webpack magic comments or Vite's manual chunking) is a further optimization when several routes share most of their code anyway."}},{"@type":"Question","name":"What's the difference between Pinia and Vuex, and why did the Vue ecosystem move toward Pinia as the standard state management library?","acceptedAnswer":{"@type":"Answer","text":"Vuex was Vue's original official state management library, built around a rigid structure of state/getters/mutations/actions, where mutations exist specifically because Vue 2's reactivity system couldn't detect certain kinds of direct state mutation, so Vuex enforced a formal 'commit a mutation' step to guarantee reactivity. Pinia is the newer official recommendation, built for Vue 3's Proxy-based reactivity, which doesn't have that same limitation — so Pinia drops the mutations concept entirely; you just call actions that directly mutate state.\n\nPinia's other practical wins: full TypeScript type inference without extra boilerplate (Vuex's typing was always clunky), a much simpler API (defineStore, no nested modules with namespacing gymnastics), each store is its own independently importable unit rather than one big nested module tree, and first-class Composition API support including the ability to write a store in a 'setup store' style that looks just like a regular composable.\n\n```js\n// Pinia — no mutations, just actions that mutate directly\nexport const useCartStore = defineStore('cart', {\n state: () => ({ items: [] }),\n actions: {\n addItem(item) {\n this.items.push(item) // direct mutation, no commit() ceremony\n },\n },\n})\n```\n\nPinia is now the Vue core team's official recommendation, and Vuex is in maintenance mode — new Vue 3 projects should default to Pinia unless there's a specific reason (like a huge existing Vuex codebase) not to."}},{"@type":"Question","name":"What does storeToRefs do in Pinia, and why is it needed when destructuring values from a store?","acceptedAnswer":{"@type":"Answer","text":"A Pinia store, when you access it via `useCartStore()`, is itself a reactive object (built on `reactive()` under the hood). Destructuring properties straight off of it — `const { items, total } = useCartStore()` — hits the exact same problem as destructuring any reactive object: you get plain, disconnected values copied out at that instant, and they stop updating when the store's state changes later.\n\n`storeToRefs()` solves this the same way `toRefs()` does for a plain reactive object — it converts each state property and getter on the store into a ref that stays linked back to the store, so destructuring is now safe.\n\n```js\nimport { storeToRefs } from 'pinia'\n\nconst store = useCartStore()\nconst { items, total } = storeToRefs(store) // reactive, live-linked refs\nconst { addItem } = store // actions are just functions — fine to destructure directly\n```\n\nThe important nuance: you only need `storeToRefs` for STATE and GETTERS (the reactive data), not for actions — actions are plain methods, not reactive data, so destructuring them directly is completely safe and doesn't lose anything. Mixing the two up (running everything through storeToRefs, or destructuring state directly without it) is a very common source of 'my component isn't updating when the store changes' bugs."}},{"@type":"Question","name":"What are functional components in Vue 3, and are they still useful given how much simpler stateful components have become?","acceptedAnswer":{"@type":"Answer","text":"A functional component is just a plain function that takes props and returns VNodes (via `h()`) — it has no instance, no reactive state of its own, no lifecycle hooks, and no `this`. In Vue 2 they existed largely as a performance optimization, since a normal Vue 2 component instance carried meaningful overhead that a plain function skipped.\n\nIn Vue 3, that overhead gap shrank a lot — Vue 3 components are already much lighter than Vue 2's, so the raw performance argument for functional components is far weaker than it used to be. They're still genuinely useful for a narrower case: purely presentational components that only ever transform props into output with zero internal state, where the plain-function form is simply less ceremony than a full SFC — think a small icon-wrapper or a formatting component.\n\n```js\nconst StatusBadge = (props) => {\n const color = props.status === 'active' ? 'green' : 'gray'\n return h('span', { class: `badge badge-${color}` }, props.status)\n}\nStatusBadge.props = ['status']\n```\n\nIn practice, most Vue 3 codebases reach for a normal `\n\n\n\n\n \n\n```\n\nIt also directly supports named/multiple v-model bindings on one component (`defineModel('title')` pairs with `v-model:title` on the parent), and options like `defineModel({ required: true, default: '' })` for prop-style validation. The net effect is that a component supporting two-way binding now reads and writes a single ref exactly like it would with plain local state, without manually re-deriving the prop/emit bridge every time."}},{"@type":"Question","name":"What is defineExpose used for in a \n```\n\n```html\n\n\n\n```\n\nThis is a genuine improvement in encapsulation over the Options API default — a component author decides its actual public surface deliberately, rather than accidentally leaking every internal variable as something a parent could reach into and depend on, which made refactoring Options API components riskier since you never knew for sure what external code might be poking at."}},{"@type":"Question","name":"What are fallthrough attributes in Vue 3, and what does setting inheritAttrs: false change about how they're handled?","acceptedAnswer":{"@type":"Answer","text":"When you pass an attribute to a component that isn't declared as one of its props (or one of its listened-for emits), Vue doesn't just discard it — by default it automatically 'falls through' and gets applied directly to the component's single root element in the rendered output. This is why you can slap a `class`, `id`, or `data-*` attribute (or even a plain native event listener like `@click`) onto a custom component and have it just work on the underlying root DOM node, without the component author needing to explicitly forward every possible HTML attribute.\n\n```html\n\n\n```\n\nSetting `inheritAttrs: false` (or, in `\n```\n\nThis matters for building wrapper components around native form elements, where you want `class`/`id`/ARIA attributes passed through to land on the actual ``, not on some outer layout div that has nothing to do with what the caller intended."}},{"@type":"Question","name":"What's the difference between onErrorCaptured and app.config.errorHandler for handling errors in a Vue application, and when would you use each?","acceptedAnswer":{"@type":"Answer","text":"`onErrorCaptured` is a component-level lifecycle hook — registering it inside a component means that component acts as an 'error boundary' for its entire subtree: any error thrown during rendering, in a lifecycle hook, or in a watcher anywhere in its descendant components bubbles up and gets caught there first. You can inspect the error, log it, and — critically — return `false` from the hook to stop the error from propagating further up the component tree, letting you render a fallback UI for just that broken sub-section while the rest of the app keeps working normally.\n\n```html\n\n\n
This widget failed to load.
\n \n\n```\n\n`app.config.errorHandler` is set once, globally, at the app instance level — it's the last-resort catch-all for any error that wasn't stopped by a closer `onErrorCaptured` boundary (or for apps that have none at all). It's the right place for app-wide concerns like reporting every uncaught error to a monitoring service (Sentry, etc.), since it's guaranteed to see anything that escapes all the way up.\n\n```js\napp.config.errorHandler = (err, instance, info) => {\n reportToSentry(err, { componentInfo: info })\n}\n```\n\nIn practice you use both together: scoped `onErrorCaptured` boundaries around specific risky/isolated widgets so one broken feature doesn't take down the whole page, and a single global `errorHandler` as the safety net that ensures nothing silently disappears without being logged somewhere."}},{"@type":"Question","name":"What is CSS v-bind() in Vue's \n```\n\nUnder the hood, Vue generates a unique CSS custom property tied to that binding, sets it as an inline style on the component's root element, and reactively updates it whenever `themeColor` changes — so the whole thing behaves like a live-updating CSS variable without you manually managing `:style` objects or toggling classes for every possible color. It's especially handy for things like theming, chart colors, or dynamic sizing that naturally belongs in CSS rather than scattered across per-element inline style bindings, while still keeping the actual value's source of truth in your reactive JavaScript state."}},{"@type":"Question","name":"What is Suspense in Vue 3, and what are its current limitations that you should know about before relying on it in production?","acceptedAnswer":{"@type":"Answer","text":"Suspense is a built-in component that lets you show a single fallback UI while waiting on asynchronous dependencies further down its tree to resolve — specifically, components using `async setup()` (or top-level `await` inside `\n\n
\n
\n \n
\n
\n\n```\n\nWhen a consumer uses ``, TypeScript infers `T` as `User` from the actual `items` array passed in, and everything downstream — the emitted event's payload type, the scoped slot's exposed data — is correctly typed as `User` rather than `any`. This matters specifically for headless/library-style components meant to be reused across many different data shapes in a single codebase, where losing type safety at the reuse boundary defeats a large part of the reason to use TypeScript in the first place."}}]}
50+ Real Questions
Vue.js Interview Questions & Answers
Commonly-asked and hard-to-find Vue.js interview questions, each with a clear, example-based answer.