Suggest an editImprove this articleRefine the answer for “Main directives in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Vue.js directives** are `v-`-prefixed attributes that apply reactive DOM behavior in templates without manual JavaScript. | Directive | What it does | |---|---| | `v-if` / `v-else` | Conditionally add or remove from DOM | | `v-show` | Toggle `display: none` | | `v-for` | Render a list (always use `:key`) | | `v-bind` / `:` | Bind attributes dynamically | | `v-on` / `@` | Attach event listeners | | `v-model` | Two-way binding for form inputs | **Key rule:** `v-if` destroys the node; `v-show` hides it with CSS. Always add `:key` to `v-for`.Shown above the full answer for quick recall.Answer (EN)Image**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-if` removes a node when its condition is false, `v-for` stamps out one copy per array item - Core six: `v-if/v-else` for conditions, `v-for` for lists, `v-bind` (`:`) for attributes, `v-on` (`@`) for events, `v-model` for forms, `v-show` for CSS toggling - `v-if` destroys the DOM node and triggers lifecycle hooks; `v-show` only flips `display: none` - Always add `:key` to `v-for` with a stable unique value, never the array index ### Quick example ```vue <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. ```vue <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. ```vue <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** ```vue <!-- 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** ```vue <!-- 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** ```vue <!-- 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-model` on `<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 ```vue <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 ```vue <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 ```vue <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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.