Main directives in Vue.js
Vue.js directives are v--prefixed template attributes that make DOM elements reactive. Instead of writing document.querySelector and toggling classes manually, you declare what should happen and Vue handles the DOM updates.
Theory
TL;DR
- Directives are sticky notes on HTML elements:
v-ifremoves a node when its condition is false,v-forstamps out one copy per array item - Core six:
v-if/v-elsefor conditions,v-forfor lists,v-bind(:) for attributes,v-on(@) for events,v-modelfor forms,v-showfor CSS toggling v-ifdestroys the DOM node and triggers lifecycle hooks;v-showonly flipsdisplay: none- Always add
:keytov-forwith a stable unique value, never the array index
Quick example
<template>
<p v-if="show">Visible!</p>
<p v-else>Hidden.</p>
<ul>
<li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true) // change to false to swap paragraphs
const fruits = ref(['Apple', 'Banana'])
</script>When show is true, the first paragraph renders and the second is not in the DOM at all. Change show to false and Vue swaps them. The list renders one <li> per item, and :key="fruit" lets Vue track each one individually.
The six directives
v-if / v-else / v-else-if add or remove elements based on a condition. When v-if turns false, Vue calls unmounted on any components inside and removes the node. When it turns true again, a fresh node is created and mounted fires.
<p v-if="isLoggedIn">Welcome!</p>
<p v-else>Please log in.</p>v-show toggles visibility with display: none. The node stays in the DOM and no lifecycle hooks fire. That makes v-show cheaper for elements that toggle frequently.
v-for renders a list by iterating over arrays or objects. The :key attribute is not optional. Without it, Vue reuses DOM nodes by position, and any reorder causes wrong state, broken inputs, and scrambled component data.
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>v-bind (shorthand :) binds a JavaScript value to an HTML attribute. :src="imageUrl" is identical to v-bind:src="imageUrl". Works on any attribute: class, style, disabled, href, custom data attributes.
v-on (shorthand @) attaches event listeners. @click="handler" replaces addEventListener('click', handler) on that element. Inline expressions work too: @click="count++".
v-model provides two-way binding for form inputs. Under the hood it is :value="text" @input="text = $event.target.value". For custom components in Vue 3, it maps to the modelValue prop and the update:modelValue emit.
v-if vs v-show: when to pick which
| v-if | v-show | |
|---|---|---|
| DOM node | Removed and recreated | Always present |
display: none | No | Yes, when hidden |
| Lifecycle hooks | Yes | No |
| Best for | Conditions that rarely change | Frequent show/hide toggles |
I once saw a team use v-if on a loading spinner that toggled 10 times per second during a polling loop. Each toggle was a full DOM destroy and recreate cycle. Switching to v-show cut that to a single style assignment per toggle.
How directives work internally
Vue's template compiler reads v- attributes at build time and converts them into render functions that return VNodes, plain JavaScript objects describing the DOM. At runtime, Vue wraps ref() values in ES6 Proxy. When a directive's render function reads a reactive value, the Proxy records that dependency. When the value changes, Vue queues a re-render, diffs the new VNode tree against the old one, and patches only the changed nodes in the real DOM.
Updates are batched per microtask tick. Change three refs in one function and Vue runs one DOM patch, not three.
Common mistakes
Using index as :key in v-for
<!-- Wrong: index key fails on reorder or item removal -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
<!-- Correct: stable unique id -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>When items reorder, index keys make Vue match DOM nodes to the wrong data. Inputs lose focus, animations play on the wrong element, component state gets mixed up.
Expecting v-else to follow v-for
<!-- Wrong: Vue errors here -->
<li v-for="item in items">{{ item }}</li>
<p v-else>No items.</p>
<!-- Correct: wrap in a v-if container -->
<template v-if="items.length">
<li v-for="item in items" :key="item.id">{{ item }}</li>
</template>
<p v-else>No items.</p>v-else must be a direct sibling of a single v-if element, not v-for.
v-if and v-for on the same element
<!-- Avoid: in Vue 3, v-if runs first and can't access v-for variables -->
<li v-for="item in items" v-if="item.active" :key="item.id">{{ item.name }}</li>
<!-- Better: filter with a computed ref -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>Filter the source array with a computed ref and loop over the result.
Real-world usage
- Nuxt.js:
v-if="status === 'pending'"shows loading spinners on async page fetches - Vuetify navigation drawers:
v-for="item in menuItems" :key="item.title"on<v-list-item> - Quasar forms:
v-modelon<q-input>for two-way binding in PWA inputs - Pinia stores:
v-for="todo in store.todos"pulls list data directly from the store into the template
Follow-up questions
Q: What is the difference between v-if and v-show?
A: v-if removes the node from the DOM when false and recreates it when true. v-show keeps the node in the DOM and toggles display: none. For frequent toggles, v-show is cheaper. For rarely shown content, v-if is better because the node does not exist at all when hidden.
Q: Why does :key matter in v-for, and why is array index a bad key?
A: Keys let Vue identify which DOM node maps to which data item. Without them, Vue matches by position. Index is unreliable for the same reason: if you remove or reorder items, indexes shift and Vue matches the wrong nodes.
Q: Can you put v-if and v-for on the same element?
A: You can, but avoid it. In Vue 3, v-if evaluates first and has no access to v-for loop variables. Move the filter into a computed ref and loop over that instead.
Q: How does v-model work under the hood?
A: It is shorthand for :value + @input on native inputs. On custom components in Vue 3, it binds the modelValue prop and listens for the update:modelValue emit.
Q: (Senior) How does Vue know which directive to re-evaluate when a reactive value changes?
A: Vue wraps reactive data in ES6 Proxy. When a directive reads a reactive value during render, the Proxy logs that dependency. When the value changes, Vue marks dependent watchers dirty and queues a re-render. The actual DOM patch runs in the next microtask tick, batching all synchronous mutations into one pass.
Examples
Basic: login state toggle
<template>
<button @click="isLoggedIn = !isLoggedIn">
{{ isLoggedIn ? 'Logout' : 'Login' }}
</button>
<p v-if="isLoggedIn">Welcome, user!</p>
<p v-else>Please sign in first.</p>
</template>
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>Button text updates reactively. The paragraphs swap on each click. The component manages one boolean and Vue handles every DOM change.
Intermediate: todo list from an API
<template>
<ul>
<li
v-for="todo in todos"
:key="todo.id"
@click="todo.done = !todo.done"
>
{{ todo.text }}
<span v-if="todo.done">✓</span>
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const todos = ref([])
onMounted(async () => {
const res = await fetch('/api/todos')
todos.value = await res.json()
})
</script>After mount, todos load from the API and v-for renders them. Clicking a row flips done. Vue updates only the <span> for that item, not the whole list.
Advanced: nested v-for with stable keys
<template>
<div v-for="user in users" :key="user.id">
<strong>{{ user.name }}</strong>
<ul>
<li v-for="task in user.tasks" :key="task.id">{{ task.text }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Alice', tasks: [{ id: 1, text: 'Design review' }, { id: 2, text: 'Write tests' }] },
{ id: 2, name: 'Bob', tasks: [{ id: 1, text: 'Deploy staging' }] }
])
</script>Both loops use id as key. If a new task is pushed to Alice's list, Vue inserts exactly one <li> without touching Bob's row. Without task.id as key, any array mutation triggers a full list rediff by position.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.