codejump

AcademyVue

Conditional Rendering and v-for Lists

v-if versus v-show is a cost trade-off, not a style choice, and v-for needs a stable :key for the same reason React needs one on map().

3 min readUpdated

3 exercises

Waiting at the end of this page.

v-if destroys and recreates; v-show only toggles CSS

Both hide an element when their expression is falsy. What they do underneath is different, and the difference has a real cost:

vue
<template>
  <!-- v-if: the element and its component instance are torn down and
       rebuilt every time the condition flips -->
  <ExpensivePanel v-if="isOpen" />

  <!-- v-show: the element stays mounted, only display:none toggles -->
  <ExpensivePanel v-show="isOpen" />
</template>

v-if is the right default: it does not pay to render something the condition usually keeps hidden, and unmounting genuinely destroys the component's state, refs and effects — useful when "closed" should mean "reset". v-show earns its place when something toggles often (a tab panel, a search box someone types into) and the cost of tearing it down and rebuilding it every keystroke — including whatever local state or focus it was holding — outweighs the memory of keeping it around. Toggling frequency, not visual similarity, decides between them.

Chaining follows the same rules as if/else if/else in JavaScript, and the elements have to be adjacent with nothing between them:

vue
<template>
  <p v-if="status === 'online'">● Online</p>
  <p v-else-if="status === 'away'">● Away</p>
  <p v-else>● Offline</p>
</template>

v-for needs a key for the same reason a React list does

v-for iterates an array or object and renders one element per item. Without a stable :key, Vue's patch algorithm falls back to matching elements by position — exactly the bug class that keying by array index reproduces in React:

vue
<template>
  <!-- Bug: keyed by position. Insert an item at the front and every
       existing element gets re-matched to the wrong data. -->
  <li v-for="(todo, index) in todos" :key="index">{{ todo.text }}</li>

  <!-- Correct: keyed by something that identifies the item itself -->
  <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</template>

The failure is invisible on a list that only ever appends to the end — index and id happen to agree there. It shows up the moment an item is inserted, removed or reordered anywhere but the tail: a form input rendered per-row now holds the wrong row's draft value, because Vue reused the DOM node at that position rather than moving the node that actually owns that data.

Deriving a filtered list keeps the source of truth singular

A "hide completed" checkbox is not new data — it is a view over the same todos array, and computed is what keeps that relationship explicit instead of duplicated:

vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const todos = ref([/* ... */])
const hideCompleted = ref(false)

const visibleTodos = computed(() =>
  hideCompleted.value ? todos.value.filter((t) => !t.done) : todos.value,
)
</script>

The alternative — a second ref holding the filtered array, kept in sync by hand on every mutation — is exactly the kind of derived state that drifts from its source the first time someone adds a new way to mutate todos and forgets to update the copy.

What interviewers check

Whether "v-if or v-show" gets an answer grounded in toggle frequency and state-reset semantics rather than a coin flip, and whether :key="index" gets flagged on sight — it is one of the most common review comments on a real Vue pull request.

Now practice it

1 quiz · 2 drills

Reading this page is the cheap half. These are the exercises that make you use it.