skip to content

In Vue 3, what happens when v-if and v-for are placed on the same element, and how did that change from Vue 2?

level: middleimportance: must knowfreq 64%

answer

  1. which directive runs first
  2. the loop alias is not in scope
  3. reversed from Vue 2
  4. wrap with <template>

basics

~20 s

In Vue 3, v-if takes priority over v-for on the same element, so the condition cannot see the loop alias; Vue 2 ran v-for first. Move the loop to a wrapping <template> or the condition to a container.

solid answer

~40 s

When both sit on one element, Vue 3 evaluates **`v-if` first**, outside the loop. A condition that reads the alias, as in `v-for="todo in todos" v-if="!todo.done"`, looks up `todo` on the component, finds nothing, and fails at render: Vue warns that the property was accessed during render but is not defined, and reading `.done` on `undefined` throws. **Vue 2 did the opposite**, running `v-for` first, so old per-item filtering code breaks after upgrading. The guide recommends never combining them on one element. For per-item conditions, put `v-for` on a `<template>` and `v-if` on the inner element (or filter in a computed); to hide the whole list, move `v-if` to the containing `<ul>`.

code

vue · 17 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const showTodos = ref(true)
const todos = ref([
  { id: 1, name: 'Write tests', isComplete: false },
  { id: 2, name: 'Ship', isComplete: true }
])
</script>

<template>
  <ul v-if="showTodos">
    <template v-for="todo in todos" :key="todo.id">
      <li v-if="!todo.isComplete">{{ todo.name }}</li>
    </template>
  </ul>
</template>

go deeper

for a junior

Remember that Vue 3 checks v-if before v-for on the same element and that the guide says not to combine them there.

for a middle

Explain why the alias is undefined inside the condition, and show the <template v-for> and container-level fixes.

for a senior

Catch this in a Vue 2 to 3 upgrade, where per-item filters silently became render errors, and restructure templates rather than patching conditions.

for a principal

Adopt a lint-enforced rule against same-element v-if and v-for so template meaning never depends on a precedence that changed between majors.

## The rule When `v-if` and `v-for` appear on the **same element**, Vue 3 gives `v-if` the **higher priority**. The condition is evaluated once, outside the loop, and only if it is truthy does the loop run. The consequence: the `v-if` expression **cannot see the loop's aliases**, because they do not exist yet. ```vue-html <!-- broken in Vue 3 --> <li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li> ``` Here `todo` in the `v-if` is resolved against the component instance, where no such property exists. In development Vue warns that the property was accessed during render but is not defined on the instance, and then `todo.isComplete` throws because `todo` is `undefined`. ## What changed from Vue 2 | | Vue 2 | Vue 3 | |---|---|---| | Same-element precedence | `v-for` first | `v-if` first | | `v-if` reading the alias | worked, filtered per item | alias undefined, render error | | `v-if` on a whole-list flag | re-evaluated for every item | evaluated once | | Official advice | avoid combining | avoid combining | The Vue 3 migration guide lists this as a breaking change. Code that relied on Vue 2's order to filter items silently changed meaning; code that used a whole-list flag kept working and became cheaper. ## The two legitimate intents and their fixes Developers combine the two directives for one of two reasons, and each has a clean replacement: 1. **Show only some items** (per-item condition). Move `v-for` onto a wrapping `<template>` and keep `v-if` on the element inside it, which makes the order explicit. Alternatively, iterate a computed list that already contains only the wanted items. 2. **Hide the whole list** (list-level condition). Move `v-if` to the container element, such as the `<ul>`, so the check reads as what it is. ```vue-html <!-- per-item: loop outside, condition inside --> <template v-for="todo in todos" :key="todo.id"> <li v-if="!todo.isComplete">{{ todo.name }}</li> </template> <!-- list-level: condition on the container --> <ul v-if="showTodos"> <li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li> </ul> ``` Note that in the `<template v-for>` form the `:key` goes on the `<template>` tag in Vue 3. ## Why the guide discourages it even when it works A same-element `v-if` that uses only a component-level flag (`v-if="showTodos"`) runs correctly in Vue 3, but it reads as a per-item filter, and a reader cannot tell which directive wins without knowing the precedence rule. Since that rule flipped between major versions, an explicit structure is safer for everyone who maintains the template. ## Checklist for an upgrade or review - Search templates for elements carrying both `v-for` and `v-if`. - If the `v-if` reads the loop alias, it is a bug in Vue 3: restructure it. - If it reads only component state, move it to the container for clarity. - In the Vue 3 migration build, a compat flag named `COMPILER_V_IF_V_FOR_PRECEDENCE` covers this change while code is being ported. ## Interview traps - **"v-if filters the list."** Only when it sits inside the loop; on the same element in Vue 3 it gates the whole loop. - **"The compiler catches it."** It does not: the template compiles, and the failure appears when the component renders. - **"`v-show` avoids the problem."** Replacing `v-if` with `v-show` on the same element makes the alias readable, but every item is still rendered and merely hidden, which is not filtering. - **"The fix is a method call in `v-if`."** A method does not change precedence; the alias is still out of scope.

  • Does `<li v-for="t in todos" v-if="showTodos">` work in Vue 3?
    Yes, because `showTodos` is component state, not the loop alias; the condition is checked once and gates the whole loop. It still goes against the guide's advice, so move the `v-if` to the `<ul>` where its meaning is obvious.
  • What should you look for when upgrading a Vue 2 app because of this precedence change?
    Every element carrying both `v-for` and a `v-if` that reads the loop alias. In Vue 2 those filtered per item; in Vue 3 the alias is undefined in the condition and rendering fails. Restructure them to `<template v-for>` with an inner `v-if`, or iterate a filtered list.

saying these in an interview costs you the question

  • In Vue 3, v-for runs before v-if on the same element.
  • A same-element v-if can filter items by reading the loop alias.
  • The precedence is the same in Vue 2 and Vue 3.
  • Putting both on one element is the recommended filtering pattern.