963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
3.8 KiB
3.8 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Never Use v-if and v-for on the Same Element | HIGH | Causes confusing precedence issues and Vue 2 to 3 migration bugs | capability |
|
Never Use v-if and v-for on the Same Element
Impact: HIGH - Using v-if and v-for on the same element creates ambiguous precedence that differs between Vue 2 and Vue 3. In Vue 2, v-for had higher precedence; in Vue 3, v-if has higher precedence. This breaking change causes subtle bugs during migration and makes code intent unclear.
The ESLint rule vue/no-use-v-if-with-v-for enforces this best practice.
Task Checklist
- Never place v-if and v-for on the same element
- For filtering list items: use a computed property that filters the array
- For hiding entire list: wrap with
<template v-if>around the v-for - Enable eslint-plugin-vue rule
vue/no-use-v-if-with-v-for
Incorrect:
<!-- WRONG: v-if and v-for on same element - ambiguous precedence -->
<template>
<!-- Intent: show only active users -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>
</template>
<!-- WRONG: Hiding entire list conditionally -->
<template>
<li v-for="user in users" v-if="shouldShowList" :key="user.id">{{ user.name }}</li>
</template>
<!-- WRONG: Vue 3 precedence issue -->
<template>
<!-- In Vue 3, v-if runs FIRST, so 'user' is undefined! -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>
<!-- Error: Cannot read property 'isActive' of undefined -->
</template>
Correct:
<!-- CORRECT: Filter with computed property -->
<template>
<li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps(['users']);
const activeUsers = computed(() => props.users.filter((user) => user.isActive));
</script>
<!-- CORRECT: Wrap with <template v-if> for conditional list -->
<template>
<template v-if="shouldShowList">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</template>
</template>
<!-- CORRECT: v-if inside the loop (per-item condition) -->
<template>
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">{{ user.name }}</li>
</template>
</ul>
</template>
Vue 2 vs Vue 3 Precedence Change
// Vue 2: v-for evaluated first
// <li v-for="user in users" v-if="user.isActive">
// Equivalent to: users.forEach(user => { if (user.isActive) render(user) })
// Vue 3: v-if evaluated first
// <li v-for="user in users" v-if="user.isActive">
// Equivalent to: if (user.isActive) users.forEach(user => render(user))
// Problem: 'user' doesn't exist yet when v-if runs!
Why Computed Properties Are Better
// Benefits of filtering via computed:
// 1. Clear separation of concerns (logic vs template)
// 2. Cached - only recalculates when dependencies change
// 3. Reusable - can be used elsewhere in component
// 4. Testable - can unit test the filtering logic
// 5. No ambiguity about intent
const activeUsers = computed(() => users.value.filter((u) => u.isActive));
// Can add more complex filtering
const filteredUsers = computed(() =>
users.value
.filter((u) => u.isActive)
.filter((u) => u.role === selectedRole.value)
.sort((a, b) => a.name.localeCompare(b.name)),
);