Skip to content
beginner Phase 2 · Template Syntax and Directives

Conditional Rendering with v-if

Show or remove elements with v-if, v-else-if, and v-show.

35m
0 problems
Topic Progress 0%

The Adjacency Rule: Why v-else Sometimes Refuses to Attach

v-if renders its element only while the expression is truthy. Everything else about the directive family is structure, and structure is where the compiler errors come from.

<template>
  <section v-if="state === 'loading'">Loading…</section>
  <section v-else-if="state === 'error'">Couldn't load. Retry?</section>
  <section v-else>{{ items.length }} items</section>
</template>

The chain reads as one conditional with branches, and the compiler treats it as exactly that: one v-if, then zero or more v-else-if, then an optional v-else, all attached to the same chain. The rule that generates most of the bug reports: each v-else/v-else-if must immediately follow its v-if sibling. Put any real element between them and the compile fails (v-else has no corresponding v-if). A stray <div> between branches is obvious; less obvious breakers are a wrapper added "just for spacing" and — depending on Vue version — comment nodes, which is why the safe habit is zero nodes of any kind between branches.

Grouping is the other structural tool. A conditional needs exactly one element unless you wrap the branches:

<!-- legal: one element per branch -->
<div v-if="loggedIn"><ProfileBadge /><Menu /></div>   <!-- no — two roots -->

<!-- legal: template collapses to no extra DOM node -->
<template v-if="loggedIn">
  <ProfileBadge />
  <Menu />
</template>

<template v-if> renders its children directly with no wrapper in the DOM, which keeps CSS layout intact (no extra flex child) while satisfying the single-element constraint. The same template tag works with v-for — the two combine into "iterate, and render each item only when…", which you will see properly decomposed in List Rendering with v-for.

A mechanism worth knowing because it changes component behavior: v-if destroys and recreates, and keys control what "recreate" means. Toggle a <UserProfile v-if="editMode"> off and on and the whole component is torn down and built fresh — setup() runs again, inputs reset to initial values, onMounted fires again. That is usually desired for "edit dialog" flows. But when two different components occupy the same position across branches — an icon switch, a chart library swap — add :key to force a full teardown instead of an in-place patch:

<component :is="currentView" :key="route.path" />   <!-- sibling approach -->
<v-chart v-if="mode === 'line'" key="line" />
<v-chart v-else key="bar" />

Without keys, Vue patches the first branch's instance when the condition flips to the second branch if the node types match, leaving state from the previous branch behind; the key guarantees each branch gets its own instance. A related pairing that prevents teardown when you want to keep state — the login panel that should remember what you typed — is exactly the v-show trade-off from the next chapter.

Finally, v-if compiles into ordinary ternaries in the render function, so a chain of N branches costs one comparison per render and instantiates only the live branch. There is no hidden template runtime: the compiler does the structural work, and the runtime only mounts or removes the chosen node.

There is a fourth branch shape worth recognizing in older code: the ternary inside interpolation, as in a Yes-or-No label. It compiles to the same render-function logic as v-if for pure text, but it cannot create or destroy elements — only choose a value. Teams that used it for whole sections ended up with deeply nested template expressions that no linter could format or reuse; the modern habit is ternaries for values, v-if for structure, and the two never compete for the same job.

v-show vs v-if: The Mount/Toggle Economics That Decide Your Frame Budget

v-show never touches the tree — it toggles the CSS display property on the element. The element, its component, its watchers, and its DOM state all exist from the moment the parent mounted; hiding is a style write.

<template>
  <HeavyTable v-if="showTable" />     <!-- created on first true, destroyed on false -->
  <HeavyTable v-show="showPanel" />   <!-- always created; toggle = el.style.display -->
</template>

That single difference drives every decision between the two:

Dimension v-if v-show
First render when true creates DOM + runs setup + mounts already created — cost paid at parent mount
When false nothing exists — zero DOM, zero listeners element present but hidden — still in DOM
Toggle cost destroy/create subtree; lifecycle hooks re-run one display write
Component hooks onMounted/onUnmounted fire per activation fire once, at parent mount
Hidden cost model pay per activation pay once, always

The decision rule, stated so you can apply it without deliberation:

  • v-show for frequent toggles. A tab strip, a collapsible sidebar, a mobile menu opened ten times a session — creating a chart component or a 200-row table on every open is waste; flipping display is a style-recalc the browser does cheaply.
  • v-if for rare or expensive branches. A modal used once per checkout, a feature gated behind a flag most users never see, an admin panel — while false, the branch costs literally nothing: no DOM nodes, no event listeners, no watcher registrations, no initialization work in setup().
  • v-if when hidden work must not happen. This is the criterion people forget: with v-show, a component's setup() runs and its onMounted fetches data even while hidden. Gate expensive or side-effectful children with v-if unless you specifically want them warm.
  • v-show when the user's state must survive hiding. The trap with v-if is data loss: type into a form, toggle it away, toggle back — the input is a brand-new DOM node with the initial value, the scroll position is gone, and any uncommitted state evaporated. For "hide but remember", either use v-show or lift the state out of the component into a store/composable so the recreated instance initializes from it.

Micro-benchmark folklore aside, the practical numbers are unambiguous: destroying and recreating a non-trivial component involves running setup, building its DOM subtree, running lifecycle hooks, and triggering layout — comfortably milliseconds to tens of milliseconds for a heavy component — while a display toggle is sub-millisecond. If a toggle stutters the UI and the template says v-if, switching to v-show is often the entire fix. Conversely, a v-show on a heavy child makes every parent mount pay for a panel that may never open; that shows up as slow first paint, which is the opposite failure.

Watch for the hybrid that covers both: v-if for existence plus v-show for an inner panel you open and close often, or lazy-loading the heavy child (defineAsyncComponent) so even the v-if activation doesn't block on a chunk download. And note the asymmetry with v-else: v-show has no v-else — chain conditions as independent directives, or use a single v-if/else chain when branches are exclusive and cheap.

The next chapter covers the third conditional-shaped trap: writing v-if on the same element as v-for, where evaluation order bites.

v-if + v-for on One Element, and Other Conditional Traps

The pattern shows up in almost every first Vue codebase:

<!-- compiles, but not the way you think -->
<li v-for="item in items" v-if="item.inStock">{{ item.name }}</li>

It "works" until it does. In Vue 3, v-if has higher priority than v-for on the same element — the compiler evaluates the condition in a scope where the iteration variable does not exist yet, warns at compile time, and at best falls back to guarding the entire list rather than each item. The correct refactor is one line of script, and it is faster as well as legal:

<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ items: { name: string; inStock: boolean }[] }>()
const inStock = computed(() => props.items.filter(i => i.inStock))
</script>

<template>
  <li v-for="item in inStock" :key="item.id">{{ item.name }}</li>
</template>

The computed filters once per source-array change instead of re-evaluating a per-item predicate on every render — and the template reads as intent ("render the in-stock items") rather than as an accident of directive precedence. When the condition depends on per-item state that changes independently, keep v-for on the outer <li> and put a v-if on an inner element, where the variable is in scope and the priority rule no longer applies.

Second trap: conditionals and DOM state. Because v-if removes nodes, everything living inside them is destroyed: input values, focus, scroll position, CSS transition state, media playback. A wizard that hides step 2 with v-if and shows it again loses everything typed there unless the values were mirrored into component state above the toggle. The fixes, in order of preference: keep the data in the parent or a store (survives recreation, keeps v-if semantics), or use v-show so the node is never destroyed. Choosing v-if for a form the user will toggle repeatedly is the classic "my inputs keep clearing" report.

Third trap: conditional branches do not share component identity.

<Editor v-if="mode === 'rich'" />
<Editor v-else mode="plain" />   <!-- different instance: state resets -->

Same component name, different branch — Vue creates a new instance when the condition flips, unless identical :key values tell it to patch in place. Decide deliberately: distinct keys for fresh state, same key (or a single component with a prop) when state should carry across.

Fourth, a rendering subtlety for templates with several roots: Vue 3 components may have multiple root nodes, and conditionals affect fragments. With v-if at multiple roots, the renderer must track the possible node shapes — a conditional root changes the fragment's children, and attribute fallthrough (covered with v-bind) can only land on a single root. The practical guidance: when a conditional is part of a component's outermost structure, wrap the whole thing in one stable element if you also expect listeners or class attributes to fall through.

None of these require new APIs — they follow from one mental model: v-if is existence, v-show is visibility. Existence is destroyed and recreated, carries no state across the gap, and evaluates before v-for when they collide; visibility preserves everything and costs a permanent DOM presence. Pick by asking what must survive the toggle and what must not run while hidden. Next: Dynamic Attributes with v-bind — conditionals choose whether something renders; bindings choose what values it renders with.

Quiz

1. A login form is toggled with `v-if`. The user types an email, hides the form, shows it again — the field is empty. What happened and what is a correct fix?

Question 1 options

2. `<li v-for="item in items" v-if="item.inStock">` warns at compile time in Vue 3. What is the correct refactor?

Question 2 options

3. Which statement about `v-show` on a component is correct?

Question 3 options

4. The template has `<section v-if="a">A</section>` … `<section v-else>B</section>` and the compiler reports that v-else has no corresponding v-if. What is the cause?

Question 4 options

Flashcards

Question

What is the mechanical difference between v-if and v-show?

Answer

v-if controls existence: false removes the element (and destroys the component instance, DOM, listeners); true creates and mounts it fresh — lifecycle hooks re-run per activation. v-show controls visibility: the element is always mounted and only el.style.display toggles, so hooks fire once and DOM/input state survives hiding.

Question

What is the adjacency rule for v-if / v-else-if / v-else, and what do you do for multi-element branches?

Answer

Each v-else-if/v-else must immediately follow its v-if sibling — any intervening node breaks the chain and the compiler errors. For branches that need multiple elements, wrap them in <template v-if>/<template v-else>, which renders children with no extra DOM node.

Question

State the decision rule between v-if and v-show.

Answer

v-show for frequent toggles (cheap display flip; tab strips, sidebars, menus) and when state must survive hiding. v-if for rare or expensive branches (nothing exists while false — no DOM, no listeners, no setup/fetch work) and whenever hidden components must not do work. v-if destroys state on hide — lift values out or use v-show if it must be remembered.

Question

Why is v-for + v-if on the same element wrong in Vue 3, and what is the fix?

Answer

v-if has higher priority: the condition evaluates before the iteration variable exists, producing a compile-time warning and per-list (not per-item) guarding. Fix by filtering in a computed and iterating the result — faster, since the filter runs once per source change instead of a predicate per item per render — or by putting v-if on an inner element inside the loop.

Question

When does a component need an explicit :key across v-if branches?

Answer

When branches occupy the same position and you want a guaranteed fresh instance instead of an in-place patch — e.g. <v-chart key="line"> vs <v-chart key="bar">, or <component :is="view" :key="route.path">. Without distinct keys, Vue may patch the first branch's instance into the second, carrying state over that belongs to the old branch.

Revision Notes

Key Takeaways

  • 1. v-if is existence (create/destroy, hooks re-run, hidden cost is zero); v-show is visibility (always mounted, one style write per toggle)
  • 2. Branch chains require immediate adjacency — the compiler attaches v-else to the nearest preceding v-if and errors otherwise
  • 3. v-if outranks v-for on one element; the legal, faster pattern is filtering in a computed before iterating
  • 4. Destruction takes state with it — input values, focus, scroll — so toggled forms need v-show or state lifted above the toggle
  • 5. :key across v-if branches decides whether switching branches reuses the instance or guarantees a fresh one

Interview Tips

  • • "v-if or v-show?" — answer with both axes: mount cost (existence vs display) and frequency (create/destroy per toggle vs one style write); add the hidden-work criterion (v-show components still run setup/fetches)
  • • "Why is v-for with v-if an anti-pattern?" — precedence: v-if evaluates before the loop variable exists; show the computed-filter refactor and note it's also faster (filter once per change vs predicate per render)
  • • "My form clears when I re-open it — what's happening?" — v-if destroyed the DOM; discuss lifting state to the parent/store vs v-show, demonstrating you connect directives to DOM lifetime
  • • "When do branches need :key?" — same-position components across v-if/else can be patched in place, carrying stale state; distinct keys force teardown — shows understanding of Vue's reconciliation

Cheat Sheet

Conditional Rendering: v-if / v-else-if / v-else / v-show

Chains:

<template v-if="state==='loading'">…</template>   <!-- no wrapper DOM node -->
<template v-else-if="state==='error'">…</template>
<template v-else>…</template>                      <!-- branches must be adjacent siblings -->

v-if vs v-show:

v-if v-show
false means removed from DOM display: none
component lifecycle destroyed/recreated per toggle mounted once with parent
hidden work none setup + onMounted still run
state on hide lost (unless lifted) preserved
best for rare/expensive branches, gated admin panels frequent toggles, remembered form state

Rules & fixes:

  • v-for + v-if same element → v-if wins; refactor to computed filter or inner v-if.
  • Multi-element branch → <template v-branch>.
  • Cross-branch instance identity → distinct :key forces fresh instances; same key/one component keeps state.
  • Conditional roots + attribute fallthrough → wrap in one stable root.

Quick picks: tab that opens 10×/session → v-show · modal per checkout → v-if · hidden component that fetches on mount → v-if (or gate the fetch) · wizard step that must keep drafts → v-show or lift state.

Continue Learning