Conditional and List Rendering

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Two directive families control what a template produces: v-if / v-show decide whether an element renders, and v-for repeats one. They follow Conditional Rendering and List Rendering.

v-if, v-else, v-else-if

v-if renders its element only when the expression is truthy. v-else-if and v-else chain from it and must sit on the immediately following sibling element.

<template>
  <p v-if="status === 'loading'">Loading...</p>
  <p v-else-if="status === 'error'">Something went wrong.</p>
  <ul v-else>
    <li v-for="item in items" :key="item.id">{{ item.label }}</li>
  </ul>
</template>

When v-if is false the element and its component children are not created; toggling it later mounts and unmounts them, running lifecycle hooks each time.

v-if on <template>

To toggle several elements as a unit without adding a wrapper, put v-if on a <template> tag. It renders its children only, no extra DOM node:

<template>
  <template v-if="showDetails">
    <h2>Details</h2>
    <p>{{ description }}</p>
  </template>
</template>

v-else / v-else-if also work on <template>.

v-if vs. v-show

v-show always renders the element and toggles its CSS display property. It has no <template> form and no v-else partner.

v-if v-show

Toggle cost

mounts / unmounts the subtree

flips one style

Initial cost

nothing rendered when false

always rendered

Best for

rarely-changing conditions, or content that should not exist

frequent toggles (menus, tabs)

Use v-show when the thing flips often; use v-if when the condition rarely changes or the branch is expensive and usually absent.

v-for

v-for iterates a data source. Always pair it with a :key.

Arrays — the item, then an optional index:

<template>
  <li v-for="(user, index) in users" :key="user.id">
    {{ index }}. {{ user.name }}
  </li>
</template>

Objects — value, then optional key, then optional index, in Object.keys() order:

<template>
  <li v-for="(value, key) in settings" :key="key">{{ key }}: {{ value }}</li>
</template>

A range — an integer n iterates 1..n:

<template>
  <span v-for="n in 5" :key="n">{{ n }}</span>   <!-- 1 2 3 4 5 -->
</template>

On <template> — repeat a group with no wrapper element:

<template>
  <template v-for="item in items" :key="item.id">
    <dt>{{ item.term }}</dt>
    <dd>{{ item.definition }}</dd>
  </template>
</template>

You can iterate an array-returning computed property to render a filtered or sorted view without mutating the source.

The key requirement

Give every v-for a :key bound to a stable, unique identifier for each item — a database id, not the array index. The key lets Vue match nodes across re-renders so it moves and reuses DOM and component state correctly instead of patching in place. An index key breaks as soon as the list is reordered, filtered, or has items inserted, and it corrupts component state and transitions. Put :key on the <template> tag when v-for is on it.

Array updates: mutate or replace

Vue detects change on a reactive array in both directions:

  • Mutation methods — push, pop, shift, unshift, splice, sort, reverse — trigger updates in place.

  • Replacement — filter, map, slice, concat return a new array; assign it back (list.value = list.value.filter(…​)) and Vue re-renders, reusing DOM where keys match.

Both are fine. Index and length writes (arr[99] = x, arr.length = 0) are also tracked in Vue 3 — see Reactivity Fundamentals for the reactivity caveats. To show a derived order without touching the original, return a copy from a computed: […​list.value].sort(…​).

v-for with a component

v-for on a component still needs a :key, and data does not flow into the component automatically — pass it as props:

<template>
  <TodoItem
    v-for="todo in todos"
    :key="todo.id"
    :todo="todo"
    @remove="removeTodo(todo.id)"
  />
</template>

Do not put v-if and v-for on the same element

It is discouraged, and the precedence is a trap: v-if has higher priority than v-for, so the condition runs before the loop variable exists, and v-if="todo.done" throws or misbehaves.

Fix it by moving one out:

<template>
  <!-- filter in a computed, then loop -->
  <li v-for="todo in pendingTodos" :key="todo.id">{{ todo.name }}</li>

  <!-- or wrap the loop in a conditional <template> -->
  <template v-if="todos.length">
    <li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li>
  </template>
</template>
const pendingTodos = computed(() => todos.value.filter((t) => !t.done))

See also