Two directives, and a surprising amount of detail behind them.
v-if
v-if puts an element in the DOM or leaves it out. When it is false the element does
not exist — it is not hidden, it is not there, and its component is not created.
<p v-if="message" class="text-secondary small mb-3">{{ message }}</p>v-else-if and v-else chain from it, and must be on the immediately
following element. The search page uses the full chain to switch between three states:
<LoadingSpinner v-if="loading" label="Searching…" />
<EmptyState
v-else-if="!results.content.length"
icon="bi-search"
title="Nothing matched"
message="Try a broader term, or clear the filters."
>
<button class="btn btn-sm btn-outline-light" @click="router.push({ name: 'explore' })">
Clear filters
</button>
</EmptyState>
<template v-else>Loading, empty, or results — exactly one renders, and the states cannot overlap. That is worth
more than it looks: writing this as three separate v-ifs eventually produces a spinner
and an empty state on screen at the same time.
v-if on a <template>
To condition a group of elements without adding a wrapper div, put the directive on a
<template>. It renders its children and nothing of itself:
<template v-if="route.query.q">Results for “<strong>{{ route.query.q }}</strong>”</template>This matters more than it sounds when you are working inside a CSS grid or a flex row, where a stray wrapper changes the layout.
v-show
v-show always renders the element and toggles display: none.
<div v-show="isOpen">Always in the DOM, sometimes display:none</div>The difference is where the cost lands. v-if is cheap to leave off and expensive to
toggle — mounting and unmounting real components. v-show is the opposite: it pays the
render cost once, up front, then toggling is one CSS property.
Use v-if by default. Reach for v-show when something
toggles often — a dropdown, a tab — and is not expensive to keep mounted. Note also that
v-show does not work on <template>, because there is no element to set
a style on.
v-for
<span v-for="tag in modelValue" :key="tag" class="tag-chip">
#{{ tag }}
</span>It also gives you an index, iterates objects, and takes a plain number:
<li v-for="(reel, index) in reels" :key="reel.id">{{ index + 1 }}. {{ reel.title }}</li>
<li v-for="(value, key) in meta" :key="key">{{ key }}: {{ value }}</li>
<span v-for="n in 5" :key="n">{{ n }}</span> <!-- 1 to 5, not 0 to 4 -->of works in place of in if you prefer it. There is no difference.
:key, and why it matters
This is the part worth slowing down for.
When a list changes, Vue reuses existing DOM elements rather than rebuilding the list. To do that
it has to work out which new item corresponds to which old element. :key is how you tell
it.
Give each item a stable, unique key — a database id, not a name that might repeat:
<div v-for="reel in results.content" :key="reel.id" class="col">
<ReelCard :reel="reel" />
</div>Why the index is usually wrong
:key="index" is the default behaviour written out by hand, and it is fine for a list
that never reorders or deletes. The moment it does, you get bugs that look like the framework is
broken.
Say you render three toasts keyed by index and dismiss the first. The second becomes index 0, so Vue decides element 0 is being updated rather than removed. Any state living in the DOM rather than in your data — an input's typed text, a video's playback position, focus, a CSS transition mid-flight — stays with the element and attaches itself to the wrong item.
The demo application's toasts are keyed :key="t.id" from an incrementing counter for
precisely this reason: they are dismissed out of order and animate as they go.
The key must be unique among siblings
Duplicate keys produce a warning and genuinely strange rendering. If you have no natural unique value, make one when you create the item rather than deriving one at render time.
Do not put v-if and v-for on the same element
<!-- WRONG -->
<li v-for="reel in reels" v-if="reel.published" :key="reel.id">{{ reel.title }}</li>In Vue 3 v-if has the higher priority, so it is evaluated first — and it references
reel, which does not exist yet. Vue warns about this.
Filter in a computed, which is faster anyway because it caches:
const published = computed(() => reels.value.filter((r) => r.published));<li v-for="reel in published" :key="reel.id">{{ reel.title }}</li>Or, if you really want it in the template, wrap with a <template v-for> and put
the v-if inside.
Arrays and reactivity
Vue 3 uses proxies, so all the mutating array methods just work — push,
splice, sort, reverse. So does assigning by index and setting
length, both of which needed workarounds in Vue 2.
reels.value.push(...res.items); // reactive
reels.value[0] = updated; // reactive in Vue 3
reels.value = reels.value.filter(f); // also fineOne thing that is not a proxy: Set and Map mutations are
tracked, but the demo application still reassigns after mutating —
liked.value = new Set(liked.value) — because a template reading
liked.has(id) depends on the identity, not the contents. If a Set-backed
computed is not updating, that is why.
Next: Components.