Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 hyf0, SerKo <https://github.com/serkodev>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,154 @@
---
name: vue-best-practices
description: MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.
license: MIT
metadata:
author: github.com/vuejs-ai
version: "18.0.0"
---
# Vue Best Practices Workflow
Use this skill as an instruction set. Follow the workflow in order unless the user explicitly asks for a different order.
## Core Principles
- **Keep state predictable:** one source of truth, derive everything else.
- **Make data flow explicit:** Props down, Events up for most cases.
- **Favor small, focused components:** easier to test, reuse, and maintain.
- **Avoid unnecessary re-renders:** use computed properties and watchers wisely.
- **Readability counts:** write clear, self-documenting code.
## 1) Confirm architecture before coding (required)
- Default stack: Vue 3 + Composition API + `<script setup lang="ts">`.
- If the project explicitly uses Options API, load `vue-options-api-best-practices` skill if available.
- If the project explicitly uses JSX, load `vue-jsx-best-practices` skill if available.
### 1.1 Must-read core references (required)
- Before implementing any Vue task, make sure to read and apply these core references:
- `references/reactivity.md`
- `references/sfc.md`
- `references/component-data-flow.md`
- `references/composables.md`
- Keep these references in active working context for the entire task, not only when a specific issue appears.
### 1.2 Plan component boundaries before coding (required)
Create a brief component map before implementation for any non-trivial feature.
- Define each component's single responsibility in one sentence.
- Keep entry/root and route-level view components as composition surfaces by default.
- Move feature UI and feature logic out of entry/root/view components unless the task is intentionally a tiny single-file demo.
- Define props/emits contracts for each child component in the map.
- Prefer a feature folder layout (`components/<feature>/...`, `composables/use<Feature>.ts`) when adding more than one component.
## 2) Apply essential Vue foundations (required)
These are essential, must-know foundations. Apply all of them in every Vue task using the core references already loaded in section `1.1`.
### Reactivity
- Must-read reference from `1.1`: [reactivity](references/reactivity.md)
- Keep source state minimal (`ref`/`reactive`), derive everything possible with `computed`.
- Use watchers for side effects if needed.
- Avoid recomputing expensive logic in templates.
### SFC structure and template safety
- Must-read reference from `1.1`: [sfc](references/sfc.md)
- Keep SFC sections in this order: `<script>``<template>``<style>`.
- Keep SFC responsibilities focused; split large components.
- Keep templates declarative; move branching/derivation to script.
- Apply Vue template safety rules (`v-html`, list rendering, conditional rendering choices).
### Keep components focused
Split a component when it has **more than one clear responsibility** (e.g. data orchestration + UI, or multiple independent UI sections).
- Prefer **smaller components + composables** over one “mega component”
- Move **UI sections** into child components (props in, events out).
- Move **state/side effects** into composables (`useXxx()`).
Apply objective split triggers. Split the component if **any** condition is true:
- It owns both orchestration/state and substantial presentational markup for multiple sections.
- It has 3+ distinct UI sections (for example: form, filters, list, footer/status).
- A template block is repeated or could become reusable (item rows, cards, list entries).
Entry/root and route view rule:
- Keep entry/root and route view components thin: app shell/layout, provider wiring, and feature composition.
- Do not place full feature implementations in entry/root/view components when those features contain independent parts.
- For CRUD/list features (todo, table, catalog, inbox), split at least into:
- feature container component
- input/form component
- list (and/or item) component
- footer/actions or filter/status component
- Allow a single-file implementation only for very small throwaway demos; if chosen, explicitly justify why splitting is unnecessary.
### Component data flow
- Must-read reference from `1.1`: [component-data-flow](references/component-data-flow.md)
- Use props down, events up as the primary model.
- Use `v-model` only for true two-way component contracts.
- Use provide/inject only for deep-tree dependencies or shared context.
- Keep contracts explicit and typed with `defineProps`, `defineEmits`, and `InjectionKey` as needed.
### Composables
- Must-read reference from `1.1`: [composables](references/composables.md)
- Extract logic into composables when it is reused, stateful, or side-effect heavy.
- Keep composable APIs small, typed, and predictable.
- Separate feature logic from presentational components.
## 3) Consider optional features only when requirements call for them
### 3.1 Standard optional features
Do not add these by default. Load the matching reference only when the requirement exists.
- Slots: parent needs to control child content/layout -> [component-slots](references/component-slots.md)
- Fallthrough attributes: wrapper/base components must forward attrs/events safely -> [component-fallthrough-attrs](references/component-fallthrough-attrs.md)
- Built-in component `<KeepAlive>` for stateful view caching -> [component-keep-alive](references/component-keep-alive.md)
- Built-in component `<Teleport>` for overlays/portals -> [component-teleport](references/component-teleport.md)
- Built-in component `<Suspense>` for async subtree fallback boundaries -> [component-suspense](references/component-suspense.md)
- Animation-related features: pick the simplest approach that matches the required motion behavior.
- Built-in component `<Transition>` for enter/leave effects -> [transition](references/component-transition.md)
- Built-in component `<TransitionGroup>` for animated list mutations -> [transition-group](references/component-transition-group.md)
- Class-based animation for non-enter/leave effects -> [animation-class-based-technique](references/animation-class-based-technique.md)
- State-driven animation for user-input-driven animation -> [animation-state-driven-technique](references/animation-state-driven-technique.md)
### 3.2 Less-common optional features
Use these only when there is explicit product or technical need.
- Directives: behavior is DOM-specific and not a good composable/component fit -> [directives](references/directives.md)
- Async components: heavy/rarely-used UI should be lazy loaded -> [component-async](references/component-async.md)
- Render functions only when templates cannot express the requirement -> [render-functions](references/render-functions.md)
- Plugins when behavior must be installed app-wide -> [plugins](references/plugins.md)
- State management patterns: app-wide shared state crosses feature boundaries -> [state-management](references/state-management.md)
## 4) Run performance optimization after behavior is correct
Performance work is a post-functionality pass. Do not optimize before core behavior is implemented and verified.
- Large list rendering bottlenecks -> [perf-virtualize-large-lists](references/perf-virtualize-large-lists.md)
- Static subtrees re-rendering unnecessarily -> [perf-v-once-v-memo-directives](references/perf-v-once-v-memo-directives.md)
- Over-abstraction in hot list paths -> [perf-avoid-component-abstraction-in-lists](references/perf-avoid-component-abstraction-in-lists.md)
- Expensive updates triggered too often -> [updated-hook-performance](references/updated-hook-performance.md)
## 5) Final self-check before finishing
- Core behavior works and matches requirements.
- All must-read references were read and applied.
- Reactivity model is minimal and predictable.
- SFC structure and template rules are followed.
- Components are focused and well-factored, splitting when needed.
- Entry/root and route view components remain composition surfaces unless there is an explicit small-demo exception.
- Component split decisions are explicit and defensible (responsibility boundaries are clear).
- Data flow contracts are explicit and typed.
- Composables are used where reuse/complexity justifies them.
- Moved state/side effects into composables if applicable
- Optional features are used only when requirements demand them.
- Performance changes were applied only after functionality was complete.

View File

@@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/vuejs-ai/skills/vue-best-practices`
- **Git SHA:** `f3dd1bf4d3ac78331bdc903e4519d561c538ca6a`
- **Synced:** 2026-03-16

View File

@@ -0,0 +1,254 @@
---
title: Use Class-based Animations for Non-Enter/Leave Effects
impact: LOW
impactDescription: Class-based animations are simpler and more performant for elements that remain in the DOM
type: best-practice
tags: [vue3, animation, css, class-binding, state]
---
# Use Class-based Animations for Non-Enter/Leave Effects
**Impact: LOW** - For animations on elements that are not entering or leaving the DOM, use CSS class-based animations triggered by Vue's reactive state. This is simpler than `<Transition>` and more appropriate for feedback animations like shake, pulse, or highlight effects.
## Task List
- Use class-based animations for elements staying in the DOM
- Use `<Transition>` only for enter/leave animations
- Combine CSS animations with Vue's class bindings (`:class`)
- Consider using `setTimeout` to auto-remove animation classes
**When to Use Class-based Animations:**
- User feedback (shake on error, pulse on success)
- Attention-grabbing effects (highlight changes)
- Hover/focus states that need more than CSS transitions
- Any animation where the element stays mounted
**When to Use Transition Component:**
- Elements entering/leaving the DOM (v-if/v-show)
- Route transitions
- List item additions/removals
## Basic Pattern
```vue
<template>
<div :class="{ shake: showError }">
<button @click="submitForm">Submit</button>
<span v-if="showError">This feature is disabled!</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showError = ref(false)
function submitForm() {
if (!isValid()) {
// Trigger shake animation
showError.value = true
// Auto-remove class after animation completes
setTimeout(() => {
showError.value = false
}, 820) // Match animation duration
}
}
</script>
<style>
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0); /* Enable GPU acceleration */
}
@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 80% { transform: translate3d(2px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(4px, 0, 0); }
}
</style>
```
## Common Animation Patterns
### Pulse on Success
```vue
<template>
<button
@click="save"
:class="{ pulse: saved }"
>
{{ saved ? 'Saved!' : 'Save' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const saved = ref(false)
async function save() {
await saveData()
saved.value = true
setTimeout(() => saved.value = false, 1000)
}
</script>
<style>
.pulse {
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
</style>
```
### Highlight on Change
```vue
<template>
<div
:class="{ highlight: justUpdated }"
>
Value: {{ value }}
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const value = ref(0)
const justUpdated = ref(false)
watch(value, () => {
justUpdated.value = true
setTimeout(() => justUpdated.value = false, 1000)
})
</script>
<style>
.highlight {
animation: highlight 1s ease-out;
}
@keyframes highlight {
0% { background-color: yellow; }
100% { background-color: transparent; }
}
</style>
```
### Bounce Attention
```vue
<template>
<div
:class="{ bounce: needsAttention }"
@animationend="needsAttention = false"
>
<BellIcon />
</div>
</template>
<script setup>
import { ref } from 'vue'
const needsAttention = ref(false)
function notifyUser() {
needsAttention.value = true
// No setTimeout needed - using animationend event
}
</script>
<style>
.bounce {
animation: bounce 0.5s ease;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
</style>
```
## Using animationend Event
Instead of `setTimeout`, use the `animationend` event for cleaner code:
```vue
<template>
<div
:class="{ animate: isAnimating }"
@animationend="isAnimating = false"
>
Content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
function triggerAnimation() {
isAnimating.value = true
// Class is automatically removed when animation ends
}
</script>
```
## Composable for Reusable Animations
```javascript
// composables/useAnimation.js
import { ref } from 'vue'
export function useAnimation(duration = 500) {
const isAnimating = ref(false)
function trigger() {
isAnimating.value = true
setTimeout(() => {
isAnimating.value = false
}, duration)
}
return {
isAnimating,
trigger
}
}
```
```vue
<script setup>
import { useAnimation } from '@/composables/useAnimation'
const shake = useAnimation(820)
const pulse = useAnimation(500)
</script>
<template>
<button
:class="{ shake: shake.isAnimating.value }"
@click="shake.trigger()"
>
Shake me
</button>
<button
:class="{ pulse: pulse.isAnimating.value }"
@click="pulse.trigger()"
>
Pulse me
</button>
</template>
```

View File

@@ -0,0 +1,291 @@
---
title: State-driven Animations with CSS Transitions and Style Bindings
impact: LOW
impactDescription: Combining Vue's reactive style bindings with CSS transitions creates smooth, interactive animations
type: best-practice
tags: [vue3, animation, css, transition, style-binding, state, interactive]
---
# State-driven Animations with CSS Transitions and Style Bindings
**Impact: LOW** - For responsive, interactive animations that react to user input or state changes, combine Vue's dynamic style bindings with CSS transitions. This creates smooth animations that interpolate values in real-time based on state.
## Task List
- Use `:style` binding for dynamic properties that change frequently
- Add CSS `transition` property to smoothly animate between values
- Consider using `transform` and `opacity` for GPU-accelerated animations
- For complex value interpolation, use watchers with animation libraries
## Basic Pattern
```vue
<template>
<div
@mousemove="onMousemove"
:style="{ backgroundColor: `hsl(${hue}, 80%, 50%)` }"
class="interactive-area"
>
<p>Move your mouse across this div...</p>
<p>Hue: {{ hue }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const hue = ref(0)
function onMousemove(e) {
// Map mouse X position to hue (0-360)
const rect = e.currentTarget.getBoundingClientRect()
hue.value = Math.round((e.clientX - rect.left) / rect.width * 360)
}
</script>
<style>
.interactive-area {
transition: background-color 0.3s ease;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
```
## Common Use Cases
### Following Mouse Position
```vue
<template>
<div
class="container"
@mousemove="onMousemove"
>
<div
class="follower"
:style="{
transform: `translate(${x}px, ${y}px)`
}"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const x = ref(0)
const y = ref(0)
function onMousemove(e) {
const rect = e.currentTarget.getBoundingClientRect()
x.value = e.clientX - rect.left
y.value = e.clientY - rect.top
}
</script>
<style>
.container {
position: relative;
height: 300px;
}
.follower {
position: absolute;
width: 20px;
height: 20px;
background: blue;
border-radius: 50%;
/* Smooth following with transition */
transition: transform 0.1s ease-out;
/* Prevent the follower from triggering mousemove */
pointer-events: none;
}
</style>
```
### Progress Animation
```vue
<template>
<div class="progress-container">
<div
class="progress-bar"
:style="{ width: `${progress}%` }"
/>
</div>
<input
type="range"
v-model.number="progress"
min="0"
max="100"
/>
</template>
<script setup>
import { ref } from 'vue'
const progress = ref(0)
</script>
<style>
.progress-container {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
transition: width 0.3s ease;
}
</style>
```
### Scroll-based Animation
```vue
<template>
<div
class="hero"
:style="{
opacity: heroOpacity,
transform: `translateY(${scrollOffset}px)`
}"
>
<h1>Scroll Down</h1>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const scrollY = ref(0)
const heroOpacity = computed(() => {
return Math.max(0, 1 - scrollY.value / 300)
})
const scrollOffset = computed(() => {
return scrollY.value * 0.5 // Parallax effect
})
function handleScroll() {
scrollY.value = window.scrollY
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<style>
.hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* Note: No transition for scroll-based animations - they should be instant */
}
</style>
```
### Color Theme Transition
```vue
<template>
<div
class="app"
:style="themeStyles"
>
<button @click="toggleTheme">Toggle Theme</button>
<p>Current theme: {{ isDark ? 'Dark' : 'Light' }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isDark = ref(false)
const themeStyles = computed(() => ({
'--bg-color': isDark.value ? '#1a1a1a' : '#ffffff',
'--text-color': isDark.value ? '#ffffff' : '#1a1a1a',
backgroundColor: 'var(--bg-color)',
color: 'var(--text-color)'
}))
function toggleTheme() {
isDark.value = !isDark.value
}
</script>
<style>
.app {
min-height: 100vh;
transition: background-color 0.5s ease, color 0.5s ease;
}
</style>
```
## Advanced: Numerical Tweening with Watchers
For smooth number animations (counters, stats), use watchers with animation libraries:
```vue
<template>
<div>
<input v-model.number="targetNumber" type="number" />
<p class="counter">{{ displayNumber.toFixed(0) }}</p>
</div>
</template>
<script setup>
import { computed, ref, reactive, watch } from 'vue'
import gsap from 'gsap'
const targetNumber = ref(0)
const tweened = reactive({ value: 0 })
// Computed for display
const displayNumber = computed(() => tweened.value)
watch(targetNumber, (newValue) => {
gsap.to(tweened, {
duration: 0.5,
value: Number(newValue) || 0,
ease: 'power2.out'
})
})
</script>
```
## Performance Considerations
```vue
<style>
/* GOOD: GPU-accelerated properties */
.element {
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* AVOID: Properties that trigger layout recalculation */
.element {
transition: width 0.3s ease, height 0.3s ease, margin 0.3s ease;
}
/* For high-frequency updates, consider will-change */
.frequently-animated {
will-change: transform;
}
</style>
```

View File

@@ -0,0 +1,97 @@
---
title: Async Component Best Practices
impact: MEDIUM
impactDescription: Poor async component strategy can delay interactivity in SSR apps and create loading UI flicker
type: best-practice
tags: [vue3, async-components, ssr, hydration, performance, ux]
---
# Async Component Best Practices
**Impact: MEDIUM** - Async components should reduce JavaScript cost without degrading perceived performance. Focus on hydration timing in SSR and stable loading UX.
## Task List
- Use lazy hydration strategies for non-critical SSR component trees
- Import only the hydration helpers you actually use
- Keep `loadingComponent` delay near the default `200ms` unless real UX data suggests otherwise
- Configure `delay` and `timeout` together for predictable loading behavior
## Use Lazy Hydration Strategies in SSR
In Vue 3.5+, async components can delay hydration until idle time, visibility, media query match, or user interaction.
**BAD:**
```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const AsyncComments = defineAsyncComponent({
loader: () => import('./Comments.vue')
})
</script>
```
**GOOD:**
```vue
<script setup lang="ts">
import {
defineAsyncComponent,
hydrateOnVisible,
hydrateOnIdle
} from 'vue'
const AsyncComments = defineAsyncComponent({
loader: () => import('./Comments.vue'),
hydrate: hydrateOnVisible({ rootMargin: '100px' })
})
const AsyncFooter = defineAsyncComponent({
loader: () => import('./Footer.vue'),
hydrate: hydrateOnIdle(5000)
})
</script>
```
## Prevent Loading Spinner Flicker
Avoid showing loading UI immediately for components that usually resolve quickly.
**BAD:**
```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
delay: 0
})
</script>
```
**GOOD:**
```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorDisplay from './ErrorDisplay.vue'
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 30000
})
</script>
```
## Delay Guidelines
| Scenario | Recommended Delay |
|----------|-------------------|
| Small component, fast network | `200ms` |
| Known heavy component | `100ms` |
| Background or non-critical UI | `300-500ms` |

View File

@@ -0,0 +1,307 @@
---
title: Component Data Flow Best Practices
impact: HIGH
impactDescription: Clear data flow between components prevents state bugs, stale UI, and brittle coupling
type: best-practice
tags: [vue3, props, emits, v-model, provide-inject, data-flow, typescript]
---
# Component Data Flow Best Practices
**Impact: HIGH** - Vue components stay reliable when data flow is explicit: props go down, events go up, `v-model` handles two-way bindings, and provide/inject supports cross-tree dependencies. Blurring these boundaries leads to stale state, hidden coupling, and hard-to-debug UI.
The main principle of data flow in Vue.js is **Props Down / Events Up**. This is the most maintainable default, and one-way flow scales well.
## Task List
- Treat props as read-only inputs
- Use props/emit for component communication; reserve refs for imperative actions
- When refs are required for imperative APIs, type them with template refs
- Emit events instead of mutating parent state directly
- Use `defineModel` for v-model in modern Vue (3.4+)
- Handle v-model modifiers deliberately in child components
- Use symbols for provide/inject keys to avoid props drilling (over ~3 layers)
- Keep mutations in the provider or expose explicit actions
- In TypeScript projects, prefer type-based `defineProps`, `defineEmits`, and `InjectionKey`
## Props: One-Way Data Down
Props are inputs. Do not mutate them in the child.
**BAD:**
```vue
<script setup>
const props = defineProps({ count: Number })
function increment() {
props.count++
}
</script>
```
**GOOD:**
If state needs to change, emit an event, use `v-model` or create a local copy.
## Prefer props/emit over component refs
**BAD:**
```vue
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const formRef = ref(null)
function submitForm() {
if (formRef.value.isValid) {
formRef.value.submit()
}
}
</script>
<template>
<UserForm ref="formRef" />
<button @click="submitForm">Submit</button>
</template>
```
**GOOD:**
```vue
<script setup>
import UserForm from './UserForm.vue'
function handleSubmit(formData) {
api.submit(formData)
}
</script>
<template>
<UserForm @submit="handleSubmit" />
</template>
```
## Type component refs when imperative access is required
Prefer props/emits by default. When a parent must call an exposed child method, type the ref explicitly and expose only the intended API from the child with `defineExpose`.
**BAD:**
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DialogPanel from './DialogPanel.vue'
const panelRef = ref(null)
onMounted(() => {
panelRef.value.open()
})
</script>
<template>
<DialogPanel ref="panelRef" />
</template>
```
**GOOD:**
```vue
<!-- DialogPanel.vue -->
<script setup lang="ts">
function open() {}
defineExpose({ open })
</script>
```
```vue
<!-- Parent.vue -->
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
import DialogPanel from './DialogPanel.vue'
// Vue 3.5+ with useTemplateRef
const panelRef = useTemplateRef('panelRef')
// Before Vue 3.5 with manual typing and ref
// const panelRef = ref<InstanceType<typeof DialogPanel> | null>(null)
onMounted(() => {
panelRef.value?.open()
})
</script>
<template>
<DialogPanel ref="panelRef" />
</template>
```
## Emits: Explicit Events Up
Component events do not bubble. If a parent needs to know about an event, re-emit it explicitly.
**BAD:**
```vue
<!-- Parent expects "saved" from grandchild, but it won't bubble -->
<Child @saved="onSaved" />
```
**GOOD:**
```vue
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['saved'])
function onGrandchildSaved(payload) {
emit('saved', payload)
}
</script>
<template>
<Grandchild @saved="onGrandchildSaved" />
</template>
```
**Event naming:** use kebab-case in templates and camelCase in script:
```vue
<script setup>
const emit = defineEmits(['updateUser'])
</script>
<template>
<ProfileForm @update-user="emit('updateUser', $event)" />
</template>
```
## `v-model`: Predictable Two-Way Bindings
Use `defineModel` by default for component bindings and emit updates on input. Only use the `modelValue` + `update:modelValue` pattern if you are on Vue < 3.4.
**BAD:**
```vue
<script setup>
const props = defineProps({ value: String })
</script>
<template>
<input :value="props.value" @input="$emit('input', $event.target.value)" />
</template>
```
**GOOD (Vue 3.4+):**
```vue
<script setup>
const model = defineModel({ type: String })
</script>
<template>
<input v-model="model" />
</template>
```
**GOOD (Vue < 3.4):**
```vue
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
```
If you need the updated value immediately after a change, use the input event value or `nextTick` in the parent.
## Provide/Inject: Shared Context Without Prop Drilling
Use provide/inject for cross-tree state, but keep mutations centralized in the provider and expose explicit actions.
**BAD:**
```vue
// Provider.vue
provide('theme', reactive({ dark: false }))
// Consumer.vue
const theme = inject('theme')
// Mutating shared state from any depth becomes hard to track
theme.dark = true
```
**GOOD:**
```vue
// Provider.vue
const theme = reactive({ dark: false })
const toggleTheme = () => { theme.dark = !theme.dark }
provide(themeKey, readonly(theme))
provide(themeActionsKey, { toggleTheme })
// Consumer.vue
const theme = inject(themeKey)
const { toggleTheme } = inject(themeActionsKey)
```
Use symbols for keys to avoid collisions in large apps:
```ts
export const themeKey = Symbol('theme')
export const themeActionsKey = Symbol('theme-actions')
```
## Use TypeScript Contracts for Public Component APIs
In TypeScript projects, type component boundaries directly with `defineProps`, `defineEmits`, and `InjectionKey` so invalid payloads and mismatched injections fail at compile time.
**BAD:**
```vue
<script setup lang="ts">
import { inject } from 'vue'
const props = defineProps({
userId: String
})
const emit = defineEmits(['save'])
const settings = inject('settings')
// Payload shape is not checked here
emit('save', 123)
// Key is string-based and not type-safe
settings?.theme = 'dark'
</script>
```
**GOOD:**
```vue
<script setup lang="ts">
import { inject, provide } from 'vue'
import type { InjectionKey } from 'vue'
interface Props {
userId: string
}
interface Emits {
save: [payload: { id: string; draft: boolean }]
}
interface Settings {
theme: 'light' | 'dark'
}
const settingsKey: InjectionKey<Settings> = Symbol('settings')
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
provide(settingsKey, { theme: 'light' })
const settings = inject(settingsKey)
if (settings) {
emit('save', { id: props.userId, draft: false })
}
</script>
```

View File

@@ -0,0 +1,174 @@
---
title: Component Fallthrough Attributes Best Practices
impact: MEDIUM
impactDescription: Incorrect $attrs access and reactivity assumptions can cause undefined values and watchers that never run
type: best-practice
tags: [vue3, attrs, fallthrough-attributes, composition-api, reactivity]
---
# Component Fallthrough Attributes Best Practices
**Impact: MEDIUM** - Fallthrough attributes are straightforward once you follow Vue's conventions: hyphenated names use bracket notation, listener keys are camelCase `onX`, and `useAttrs()` is current-but-not-reactive.
## Task List
- Access hyphenated attribute names with bracket notation (for example `attrs['data-testid']`)
- Access event listeners with camelCase `onX` keys (for example `attrs.onClick`)
- Do not `watch()` values returned from `useAttrs()`; those watchers do not trigger on attr changes
- Use `onUpdated()` for attr-driven side effects
- Promote frequently observed attrs to props when reactive observation is required
## Access Attribute and Listener Keys Correctly
Hyphenated attribute names preserve their original casing in JavaScript, so dot notation does not work for keys that include `-`.
**BAD:**
```vue
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs.data-testid) // Syntax error
console.log(attrs.dataTestid) // undefined for data-testid
console.log(attrs['on-click']) // undefined
console.log(attrs['@click']) // undefined
</script>
```
**GOOD:**
```vue
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs['data-testid'])
console.log(attrs['aria-label'])
console.log(attrs['foo-bar'])
console.log(attrs.onClick)
console.log(attrs.onCustomEvent)
console.log(attrs.onMouseEnter)
</script>
```
### Naming Reference
| Parent Usage | Access in `attrs` |
|--------------|-------------------|
| `class="foo"` | `attrs.class` |
| `data-id="123"` | `attrs['data-id']` |
| `aria-label="..."` | `attrs['aria-label']` |
| `foo-bar="baz"` | `attrs['foo-bar']` |
| `@click="fn"` | `attrs.onClick` |
| `@custom-event="fn"` | `attrs.onCustomEvent` |
| `@update:modelValue="fn"` | `attrs['onUpdate:modelValue']` |
## `useAttrs()` Is Not Reactive
`useAttrs()` always reflects the latest values, but it is intentionally not reactive for watcher tracking.
**BAD:**
```vue
<script setup>
import { watch, watchEffect, useAttrs } from 'vue'
const attrs = useAttrs()
watch(
() => attrs.someAttr,
(newValue) => {
console.log('Changed:', newValue) // Never runs on attr changes
}
)
watchEffect(() => {
console.log(attrs.class) // Runs on setup, not on attr updates
})
</script>
```
**GOOD:**
```vue
<script setup>
import { onUpdated, useAttrs } from 'vue'
const attrs = useAttrs()
onUpdated(() => {
console.log('Latest attrs:', attrs)
})
</script>
```
**GOOD:**
```vue
<script setup>
import { watch } from 'vue'
const props = defineProps({
someAttr: String
})
watch(
() => props.someAttr,
(newValue) => {
console.log('Changed:', newValue)
}
)
</script>
```
## Common Patterns
### Check for optional attrs safely
```vue
<script setup>
import { computed, useAttrs } from 'vue'
const attrs = useAttrs()
const hasTestId = computed(() => 'data-testid' in attrs)
const ariaLabel = computed(() => attrs['aria-label'] ?? 'Default label')
</script>
```
### Forward listeners after internal logic
```vue
<script setup>
import { useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const attrs = useAttrs()
function handleClick(event) {
console.log('Internal handling first')
attrs.onClick?.(event)
}
</script>
<template>
<button @click="handleClick">
<slot />
</button>
</template>
```
## TypeScript Notes
`useAttrs()` is typed as `Record<string, unknown>`, so cast individual keys when needed.
```vue
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
const testId = attrs['data-testid'] as string | undefined
const onClick = attrs.onClick as ((event: MouseEvent) => void) | undefined
</script>
```

View File

@@ -0,0 +1,137 @@
---
title: KeepAlive Component Best Practices
impact: HIGH
impactDescription: KeepAlive caches component instances; misuse causes stale data, memory growth, or unexpected lifecycle behavior
type: best-practice
tags: [vue3, keepalive, cache, performance, router, dynamic-components]
---
# KeepAlive Component Best Practices
**Impact: HIGH** - `<KeepAlive>` caches component instances instead of destroying them. Use it to preserve state across switches, but manage cache size and freshness explicitly to avoid memory growth or stale UI.
## Task List
- Use KeepAlive only where state preservation improves UX
- Set a reasonable `max` to cap cache size
- Declare component names for include/exclude matching
- Use `onActivated`/`onDeactivated` for cache-aware logic
- Decide how and when cached views refresh their data
- Avoid caching memory-heavy or security-sensitive views
## When to Use KeepAlive
Use KeepAlive when switching between views where state should persist (tabs, multi-step forms, dashboards). Avoid it when each visit should start fresh.
**BAD:**
```vue
<template>
<!-- State resets on every switch -->
<component :is="currentTab" />
</template>
```
**GOOD:**
```vue
<template>
<!-- State preserved between switches -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>
```
## When NOT to Use KeepAlive
- Search or filter pages where users expect fresh results
- Memory-heavy components (maps, large tables, media players)
- Sensitive flows where data must be cleared on exit
- Components with heavy background activity you cannot pause
## Limit and Control the Cache
Always cap cache size with `max` and restrict caching to specific components when possible.
```vue
<template>
<KeepAlive :max="5" include="Dashboard,Settings">
<component :is="currentView" />
</KeepAlive>
</template>
```
## Ensure Component Names Match include/exclude
`include` and `exclude` match the component `name` option. Explicitly set names for reliable caching.
```vue
<!-- TabA.vue -->
<script setup>
defineOptions({ name: 'TabA' })
</script>
```
```vue
<template>
<KeepAlive include="TabA,TabB">
<component :is="currentTab" />
</KeepAlive>
</template>
```
## Cache Invalidation Strategies
Vue 3 has no direct API to remove a specific cached instance. Use keys or dynamic include/exclude to force refreshes.
```vue
<script setup>
import { ref, reactive } from 'vue'
const currentView = ref('Dashboard')
const viewKeys = reactive({ Dashboard: 0, Settings: 0 })
function invalidateCache(view) {
viewKeys[view]++
}
</script>
<template>
<KeepAlive>
<component :is="currentView" :key="`${currentView}-${viewKeys[currentView]}`" />
</KeepAlive>
</template>
```
## Lifecycle Hooks for Cached Components
Cached components are not destroyed on switch. Use activation hooks for refresh and cleanup.
```vue
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
refreshData()
})
onDeactivated(() => {
pauseTimers()
})
</script>
```
## Router Caching and Freshness
Decide whether navigation should show cached state or a fresh view. A common pattern is to key by route when params change.
```vue
<template>
<router-view v-slot="{ Component, route }">
<KeepAlive>
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</router-view>
</template>
```
If you want cache reuse but fresh data, refresh in `onActivated` and compare query/params before fetching.

View File

@@ -0,0 +1,216 @@
---
title: Component Slots Best Practices
impact: MEDIUM
impactDescription: Poor slot API design causes empty DOM wrappers, weak TypeScript safety, brittle defaults, and unnecessary component overhead
type: best-practice
tags: [vue3, slots, components, typescript, composables]
---
# Component Slots Best Practices
**Impact: MEDIUM** - Slots are a core component API surface in Vue. Structure them intentionally so templates stay predictable, typed, and performant.
## Task List
- Use shorthand syntax for named slots (`#` instead of `v-slot:`)
- Render optional slot wrapper elements only when slot content exists (`$slots` checks)
- Type scoped slot contracts with `defineSlots` in TypeScript components
- Provide fallback content for optional slots
- Prefer composables over renderless components for pure logic reuse
## Shorthand syntax for named slots
**BAD:**
```vue
<MyComponent>
<template v-slot:header> ... </template>
</MyComponent>
```
**GOOD:**
```vue
<MyComponent>
<template #header> ... </template>
</MyComponent>
```
## Conditionally Render Optional Slot Wrappers
Use `$slots` checks when wrapper elements add spacing, borders, or layout constraints.
**BAD:**
```vue
<!-- Card.vue -->
<template>
<article class="card">
<header class="card-header">
<slot name="header" />
</header>
<section class="card-body">
<slot />
</section>
<footer class="card-footer">
<slot name="footer" />
</footer>
</article>
</template>
```
**GOOD:**
```vue
<!-- Card.vue -->
<template>
<article class="card">
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<section v-if="$slots.default" class="card-body">
<slot />
</section>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</article>
</template>
```
## Type Scoped Slot Props with defineSlots
In `<script setup lang="ts">`, use `defineSlots` so slot consumers get autocomplete and static checks.
**BAD:**
```vue
<!-- ProductList.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
}
defineProps<{ products: Product[] }>()
</script>
<template>
<ul>
<li v-for="(product, index) in products" :key="product.id">
<slot :product="product" :index="index" />
</li>
</ul>
</template>
```
**GOOD:**
```vue
<!-- ProductList.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
}
defineProps<{ products: Product[] }>()
defineSlots<{
default(props: { product: Product; index: number }): any
empty(): any
}>()
</script>
<template>
<ul v-if="products.length">
<li v-for="(product, index) in products" :key="product.id">
<slot :product="product" :index="index" />
</li>
</ul>
<slot v-else name="empty" />
</template>
```
## Provide Slot Fallback Content
Fallback content makes components resilient when parents omit optional slots.
**BAD:**
```vue
<!-- SubmitButton.vue -->
<template>
<button type="submit" class="btn-primary">
<slot />
</button>
</template>
```
**GOOD:**
```vue
<!-- SubmitButton.vue -->
<template>
<button type="submit" class="btn-primary">
<slot>Submit</slot>
</button>
</template>
```
## Prefer Composables for Pure Logic Reuse
Renderless components are still useful for slot-driven composition, but composables are usually cleaner for logic-only reuse.
**BAD:**
```vue
<!-- MouseTracker.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function onMove(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
</script>
<template>
<slot :x="x" :y="y" />
</template>
```
**GOOD:**
```ts
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function onMove(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
return { x, y }
}
```
```vue
<!-- MousePosition.vue -->
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>{{ x }}, {{ y }}</p>
</template>
```

View File

@@ -0,0 +1,228 @@
---
title: Suspense Component Best Practices
impact: MEDIUM
impactDescription: Suspense coordinates async dependencies with fallback UI; misconfiguration leads to missing loading states or confusing UX
type: best-practice
tags: [vue3, suspense, async-components, async-setup, loading, fallback, router, transition, keepalive]
---
# Suspense Component Best Practices
**Impact: MEDIUM** - `<Suspense>` coordinates async dependencies (async components or async setup) and renders a fallback while they resolve. Misconfiguration leads to missing loading states, empty renders, or subtle UX bugs.
## Task List
- Wrap default and fallback slot content in a single root node
- Use `timeout` when you need the fallback to appear on reverts
- Force root replacement with `:key` when you need Suspense to re-trigger
- Add `suspensible` to nested Suspense boundaries (Vue 3.3+)
- Use `@pending`, `@resolve`, and `@fallback` for programmatic loading state
- Nest `RouterView` -> `Transition` -> `KeepAlive` -> `Suspense` in that order
- Keep Suspense usage centralized and documented in production
## Single Root in Default and Fallback Slots
Suspense tracks a single immediate child in both slots. Wrap multiple elements in a single element or component.
**BAD:**
```vue
<template>
<Suspense>
<AsyncHeader />
<AsyncList />
<template #fallback>
<LoadingSpinner />
<LoadingHint />
</template>
</Suspense>
</template>
```
**GOOD:**
```vue
<template>
<Suspense>
<div>
<AsyncHeader />
<AsyncList />
</div>
<template #fallback>
<div>
<LoadingSpinner />
<LoadingHint />
</div>
</template>
</Suspense>
</template>
```
## Fallback Timing on Reverts (`timeout`)
When Suspense is already resolved and new async work starts, the previous content remains visible until the timeout elapses. Use `timeout="0"` for immediate fallback or a short delay to avoid flicker.
**BAD:**
```vue
<template>
<Suspense>
<component :is="currentView" :key="viewKey" />
<template #fallback>
Loading...
</template>
</Suspense>
</template>
```
**GOOD:**
```vue
<template>
<Suspense :timeout="200">
<component :is="currentView" :key="viewKey" />
<template #fallback>
Loading...
</template>
</Suspense>
</template>
```
## Pending State Only Re-triggers on Root Replacement
Once resolved, Suspense only re-enters pending when the root node of the default slot changes. If async work happens deeper in the tree, no fallback appears.
**BAD:**
```vue
<template>
<Suspense>
<TabContainer>
<AsyncDashboard v-if="tab === 'dashboard'" />
<AsyncSettings v-else />
</TabContainer>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
```
**GOOD:**
```vue
<template>
<Suspense>
<component :is="tabs[tab]" :key="tab" />
<template #fallback>
Loading...
</template>
</Suspense>
</template>
```
## Use `suspensible` for Nested Suspense (Vue 3.3+)
Nested Suspense boundaries need `suspensible` on the inner boundary so the parent can coordinate loading state. Without it, inner async content may render empty nodes until resolved.
**BAD:**
```vue
<template>
<Suspense>
<LayoutShell>
<Suspense>
<AsyncWidget />
<template #fallback>Loading widget...</template>
</Suspense>
</LayoutShell>
<template #fallback>Loading layout...</template>
</Suspense>
</template>
```
**GOOD:**
```vue
<template>
<Suspense>
<LayoutShell>
<Suspense suspensible>
<AsyncWidget />
<template #fallback>Loading widget...</template>
</Suspense>
</LayoutShell>
<template #fallback>Loading layout...</template>
</Suspense>
</template>
```
## Track Loading with Suspense Events
Use `@pending`, `@resolve`, and `@fallback` for analytics, global loading indicators, or coordinating UI outside the Suspense boundary.
```vue
<script setup>
import { ref } from 'vue'
const isLoading = ref(false)
const onPending = () => {
isLoading.value = true
}
const onResolve = () => {
isLoading.value = false
}
</script>
<template>
<LoadingBar v-if="isLoading" />
<Suspense @pending="onPending" @resolve="onResolve">
<AsyncPage />
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</template>
```
## Recommended Nesting with RouterView, Transition, KeepAlive
When combining these components, the nesting order should be `RouterView` -> `Transition` -> `KeepAlive` -> `Suspense` so each wrapper works correctly.
**BAD:**
```vue
<template>
<RouterView v-slot="{ Component }">
<Suspense>
<KeepAlive>
<Transition mode="out-in">
<component :is="Component" />
</Transition>
</KeepAlive>
</Suspense>
</RouterView>
</template>
```
**GOOD:**
```vue
<template>
<RouterView v-slot="{ Component }">
<Transition mode="out-in">
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>Loading...</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
</template>
```
## Treat Suspense Cautiously in Production
In production code, keep Suspense boundaries minimal, document where they are used, and have a fallback loading strategy if you ever need to replace or refactor them.

View File

@@ -0,0 +1,108 @@
---
title: Teleport Component Best Practices
impact: MEDIUM
impactDescription: Teleport renders content outside the component's DOM position, which is essential for overlays but affects styling and layout
type: best-practice
tags: [vue3, teleport, modal, overlay, positioning, responsive]
---
# Teleport Component Best Practices
**Impact: MEDIUM** - `<Teleport>` renders part of a component's template in a different place in the DOM while preserving the Vue component hierarchy. Use it for overlays (modals, toasts, tooltips) or any UI that must escape stacking contexts, overflow, or fixed positioning constraints.
## Task List
- Teleport overlays to `body` or a dedicated container outside the app root
- Keep a shared target for similar UI (`#modals`, `#notifications`) and control layering with order or z-index
- Use `:disabled` for responsive layouts that should render inline on small screens
- Remember props, emits, and provide/inject still work through teleport
- Avoid relying on parent stacking contexts or transforms for teleported UI
## Teleport Overlays Out of Transformed Containers
When an ancestor has `transform`, `filter`, or `perspective`, fixed-position overlays can behave like they are locally positioned. Teleport escapes that context.
**BAD:**
```vue
<template>
<div class="animated-container">
<button @click="open = true">Open</button>
<!-- Broken: fixed positioning is scoped to the transformed parent -->
<div v-if="open" class="modal">Modal</div>
</div>
</template>
<style>
.animated-container {
transform: translateZ(0);
}
.modal {
position: fixed;
inset: 0;
z-index: 9999;
}
</style>
```
**GOOD:**
```vue
<template>
<div class="animated-container">
<button @click="open = true">Open</button>
<Teleport to="body">
<div v-if="open" class="modal">Modal</div>
</Teleport>
</div>
</template>
```
## Responsive Layouts with `disabled`
Use `:disabled` to render inline on mobile and teleport on larger screens:
```vue
<script setup>
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 768px)')
</script>
<template>
<Teleport to="body" :disabled="isMobile">
<nav class="sidebar">Navigation</nav>
</Teleport>
</template>
```
## Logical Hierarchy Is Preserved
Teleport changes DOM position, not the Vue component tree. Props, emits, slots, and provide/inject still work:
```vue
<template>
<Teleport to="body">
<ChildPanel :message="message" @close="open = false" />
</Teleport>
</template>
```
## Multiple Teleports to the Same Target
Teleports to the same target append in declaration order:
```vue
<template>
<Teleport to="#notifications">
<div>First</div>
</Teleport>
<Teleport to="#notifications">
<div>Second</div>
</Teleport>
</template>
```
Use a shared container to keep stacking predictable, and apply z-index only when you need explicit layering.

View File

@@ -0,0 +1,128 @@
---
title: TransitionGroup Component Best Practices
impact: MEDIUM
impactDescription: TransitionGroup animates list items; missing keys or misuse leads to broken list transitions
type: best-practice
tags: [vue3, transition-group, animation, lists, keys]
---
# TransitionGroup Component Best Practices
**Impact: MEDIUM** - `<TransitionGroup>` animates lists of items entering, leaving, and moving. Use it for `v-for` lists or dynamic collections where individual items change over time.
## Task List
- Use `<TransitionGroup>` only for lists and repeated items
- Provide unique, stable keys for every direct child
- Use `tag` when you need semantic or layout wrappers
- Avoid the `mode` prop (not supported)
- Use JavaScript hooks for staggered effects
## Use TransitionGroup for Lists
`<TransitionGroup>` is designed for list items. Use `tag` to control the wrapper element when needed.
**BAD:**
```vue
<template>
<TransitionGroup name="fade">
<ComponentA />
<ComponentB />
</TransitionGroup>
</template>
```
**GOOD:**
```vue
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</TransitionGroup>
</template>
```
## Always Provide Stable Keys
Keys are required. Without stable keys, Vue cannot track item positions and animations break.
**BAD:**
```vue
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
</li>
</TransitionGroup>
</template>
```
**GOOD:**
```vue
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</TransitionGroup>
</template>
```
## Do Not Use `mode` on TransitionGroup
`mode` is only for `<Transition>` because it swaps a single element. Use `<Transition>` if you need in/out sequencing.
**BAD:**
```vue
<template>
<TransitionGroup name="list" tag="div" mode="out-in">
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</TransitionGroup>
</template>
```
**GOOD:**
```vue
<template>
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>
</template>
```
## Stagger List Animations with Data Attributes
For cascading list animations, pass the index to JavaScript hooks and compute delay per item.
```vue
<template>
<TransitionGroup
tag="ul"
:css="false"
@before-enter="onBeforeEnter"
@enter="onEnter"
>
<li v-for="(item, index) in items" :key="item.id" :data-index="index">
{{ item.name }}
</li>
</TransitionGroup>
</template>
<script setup>
function onBeforeEnter(el) {
el.style.opacity = 0
el.style.transform = 'translateY(12px)'
}
function onEnter(el, done) {
const delay = Number(el.dataset.index) * 80
setTimeout(() => {
el.style.transition = 'all 0.25s ease'
el.style.opacity = 1
el.style.transform = 'translateY(0)'
setTimeout(done, 250)
}, delay)
}
</script>
```

View File

@@ -0,0 +1,125 @@
---
title: Transition Component Best Practices
impact: MEDIUM
impactDescription: Transition animates a single element or component; incorrect structure or keys prevent animations
type: best-practice
tags: [vue3, transition, animation, performance, keys]
---
# Transition Component Best Practices
**Impact: MEDIUM** - `<Transition>` animates entering/leaving of a single element or component. It is ideal for toggling UI states, swapping views, or animating one component at a time.
## Task List
- Wrap a single element or component inside `<Transition>`
- Provide a `key` when switching between same element types
- Use `mode="out-in"` when you need sequential swaps
- Prefer `transform` and `opacity` for smooth animations
## Use Transition for a Single Root Element
`<Transition>` only supports one direct child. Wrap multiple nodes in a single element or component.
**BAD:**
```vue
<template>
<Transition name="fade">
<h3>Title</h3>
<p>Description</p>
</Transition>
</template>
```
**GOOD:**
```vue
<template>
<Transition name="fade">
<div>
<h3>Title</h3>
<p>Description</p>
</div>
</Transition>
</template>
```
## Force Transitions Between Same Element Types
Vue reuses the same DOM element when the tag type does not change. Add `key` so Vue treats it as a new element and triggers enter/leave.
**BAD:**
```vue
<template>
<Transition name="fade">
<p v-if="isActive">Active</p>
<p v-else>Inactive</p>
</Transition>
</template>
```
**GOOD:**
```vue
<template>
<Transition name="fade" mode="out-in">
<p v-if="isActive" key="active">Active</p>
<p v-else key="inactive">Inactive</p>
</Transition>
</template>
```
## Use `mode` to Avoid Overlap During Swaps
When swapping components or views, use `mode="out-in"` to prevent both from being visible at the same time.
**BAD:**
```vue
<template>
<Transition name="fade">
<component :is="currentView" />
</Transition>
</template>
```
**GOOD:**
```vue
<template>
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>
</template>
```
## Animate `transform` and `opacity` for Performance
Avoid layout-triggering properties such as `height`, `margin`, or `top`. Use `transform` and `opacity` for smooth, GPU-friendly transitions.
**BAD:**
```css
.slide-enter-active,
.slide-leave-active {
transition: height 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
height: 0;
}
```
**GOOD:**
```css
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
transform: translateX(-12px);
opacity: 0;
}
.slide-leave-to {
transform: translateX(12px);
opacity: 0;
}
```

View File

@@ -0,0 +1,290 @@
---
title: Composable Organization Patterns
impact: MEDIUM
impactDescription: Well-structured composables improve maintainability, reusability, and update performance
type: best-practice
tags: [vue3, composables, composition-api, code-organization, api-design, readonly, utilities]
---
# Composable Organization Patterns
**Impact: MEDIUM** - Treat composables as reusable, stateful building blocks and keep their code organized by feature concern. This keeps large components maintainable and prevents hard-to-debug mutation and API design issues.
## Task List
- Compose complex behavior from small, focused composables
- Use options objects for composables with multiple optional parameters
- Return readonly state when updates must flow through explicit actions
- Keep pure utility functions as plain utilities, not composables
- Organize composable and component code by feature concern, and extract composables when components grow
## Compose Composables from Smaller Primitives
**BAD:**
```vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const inside = ref(false)
const el = ref(null)
function onMove(e) {
x.value = e.pageX
y.value = e.pageY
if (!el.value) return
const r = el.value.getBoundingClientRect()
inside.value = x.value >= r.left && x.value <= r.right &&
y.value >= r.top && y.value <= r.bottom
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
</script>
```
**GOOD:**
```javascript
// composables/useEventListener.js
import { onMounted, onUnmounted, toValue } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => toValue(target).addEventListener(event, callback))
onUnmounted(() => toValue(target).removeEventListener(event, callback))
}
```
```javascript
// composables/useMouse.js
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (e) => {
x.value = e.pageX
y.value = e.pageY
})
return { x, y }
}
```
```javascript
// composables/useMouseInElement.js
import { computed } from 'vue'
import { useMouse } from './useMouse'
export function useMouseInElement(elementRef) {
const { x, y } = useMouse()
const isOutside = computed(() => {
if (!elementRef.value) return true
const rect = elementRef.value.getBoundingClientRect()
return x.value < rect.left || x.value > rect.right ||
y.value < rect.top || y.value > rect.bottom
})
return { x, y, isOutside }
}
```
## Use Options Object Pattern for Composable Parameters
**BAD:**
```javascript
export function useFetch(url, method, headers, timeout, retries, immediate) {
// hard to read and easy to misorder
}
useFetch('/api/users', 'GET', null, 5000, 3, true)
```
**GOOD:**
```javascript
export function useFetch(url, options = {}) {
const {
method = 'GET',
headers = {},
timeout = 30000,
retries = 0,
immediate = true
} = options
// implementation
return { method, headers, timeout, retries, immediate }
}
useFetch('/api/users', {
method: 'POST',
timeout: 5000,
retries: 3
})
```
```typescript
interface UseCounterOptions {
initial?: number
min?: number
max?: number
step?: number
}
export function useCounter(options: UseCounterOptions = {}) {
const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options
// implementation
}
```
## Return Readonly State with Explicit Actions
**BAD:**
```javascript
export function useCart() {
const items = ref([])
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))
return { items, total } // any consumer can mutate directly
}
const { items } = useCart()
items.value.push({ id: 1, price: 10 })
```
**GOOD:**
```javascript
import { ref, computed, readonly } from 'vue'
export function useCart() {
const _items = ref([])
const total = computed(() =>
_items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product, quantity = 1) {
const existing = _items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity += quantity
return
}
_items.value.push({ ...product, quantity })
}
function removeItem(productId) {
_items.value = _items.value.filter(item => item.id !== productId)
}
return {
items: readonly(_items),
total,
addItem,
removeItem
}
}
```
## Keep Utilities as Utilities
**BAD:**
```javascript
export function useFormatters() {
const formatDate = (date) => new Intl.DateTimeFormat('en-US').format(date)
const formatCurrency = (amount) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
return { formatDate, formatCurrency }
}
const { formatDate } = useFormatters()
```
**GOOD:**
```javascript
// utils/formatters.js
export function formatDate(date) {
return new Intl.DateTimeFormat('en-US').format(date)
}
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
```
```javascript
// composables/useInvoiceSummary.js
import { computed } from 'vue'
import { formatCurrency } from '@/utils/formatters'
export function useInvoiceSummary(invoiceRef) {
const totalLabel = computed(() => formatCurrency(invoiceRef.value.total))
return { totalLabel }
}
```
## Organize Composable and Component Code by Feature Concern
**BAD:**
```vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const searchQuery = ref('')
const items = ref([])
const selected = ref(null)
const showModal = ref(false)
const sortBy = ref('name')
const filter = ref('all')
const loading = ref(false)
const filtered = computed(() => items.value.filter(i => i.category === filter.value))
function openModal() { showModal.value = true }
const sorted = computed(() => [...filtered.value].sort(/* ... */))
watch(searchQuery, () => { /* ... */ })
onMounted(() => { /* ... */ })
</script>
```
**GOOD:**
```vue
<script setup>
import { useItems } from '@/composables/useItems'
import { useSearch } from '@/composables/useSearch'
import { useSelectionModal } from '@/composables/useSelectionModal'
// Data
const { items, loading, fetchItems } = useItems()
// Search/filter/sort
const { query, visibleItems } = useSearch(items)
// Selection + modal
const { selectedItem, isModalOpen, selectItem, closeModal } = useSelectionModal()
</script>
```
```javascript
// composables/useItems.js
import { ref, onMounted } from 'vue'
export function useItems() {
const items = ref([])
const loading = ref(false)
async function fetchItems() {
loading.value = true
try {
items.value = await api.getItems()
} finally {
loading.value = false
}
}
onMounted(fetchItems)
return { items, loading, fetchItems }
}
```

View File

@@ -0,0 +1,162 @@
---
title: Directive Best Practices
impact: MEDIUM
impactDescription: Custom directives are powerful but easy to misuse; following patterns prevents leaks, invalid usage, and unclear abstractions
type: best-practice
tags: [vue3, directives, custom-directives, composition, typescript]
---
# Directive Best Practices
**Impact: MEDIUM** - Directives are for low-level DOM access. Use them sparingly, keep them side-effect safe, and prefer components or composables when you need stateful or reusable UI behavior.
## Task List
- Use directives only when you need direct DOM access
- Do not mutate directive arguments or binding objects
- Clean up timers, listeners, and observers in `unmounted`
- Register directives in `<script setup>` with the `v-` prefix
- In TypeScript projects, type directive values and augment template directive types
- Prefer components or composables for complex behavior
## Treat Directive Arguments as Read-Only
Directive bindings are not reactive storage. Dont write to them.
```ts
const vFocus = {
mounted(el, binding) {
// binding.value is read-only
el.focus()
}
}
```
## Avoid Directives on Components
Directives apply to DOM elements. When used on components, they attach to the root element and can break if the root changes.
**BAD:**
```vue
<MyInput v-focus />
```
**GOOD:**
```vue
<!-- MyInput.vue -->
<script setup>
const vFocus = (el) => el.focus()
</script>
<template>
<input v-focus />
</template>
```
## Clean Up Side Effects in `unmounted`
Any timers, listeners, or observers must be removed to avoid leaks.
```ts
const vResize = {
mounted(el) {
const observer = new ResizeObserver(() => {})
observer.observe(el)
el._observer = observer
},
unmounted(el) {
el._observer?.disconnect()
}
}
```
## Prefer Function Shorthand for Single-Hook Directives
If you only need `mounted`/`updated`, use the function form.
```ts
const vAutofocus = (el) => el.focus()
```
## Use the `v-` Prefix and Script Setup Registration
```vue
<script setup>
const vFocus = (el) => el.focus()
</script>
<template>
<input v-focus />
</template>
```
## Type Custom Directives in TypeScript Projects
Use `Directive<Element, ValueType>` so `binding.value` is typed, and augment Vue's template types so directives are recognized in SFC templates.
**BAD:**
```ts
// Untyped directive value and no template type augmentation
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
}
```
**GOOD:**
```ts
import type { Directive } from 'vue'
type HighlightValue = string
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
} satisfies Directive<HTMLElement, HighlightValue>
declare module 'vue' {
interface ComponentCustomProperties {
vHighlight: typeof vHighlight
}
}
```
## Handle SSR with `getSSRProps`
Directive hooks such as `mounted` and `updated` do not run during SSR. If a directive sets attributes/classes that affect rendered HTML, provide an SSR equivalent via `getSSRProps` to avoid hydration mismatches.
**BAD:**
```ts
const vTooltip = {
mounted(el, binding) {
el.setAttribute('data-tooltip', binding.value)
el.classList.add('has-tooltip')
}
}
```
**GOOD:**
```ts
const vTooltip = {
mounted(el, binding) {
el.setAttribute('data-tooltip', binding.value)
el.classList.add('has-tooltip')
},
getSSRProps(binding) {
return {
'data-tooltip': binding.value,
class: 'has-tooltip'
}
}
}
```
## Prefer Declarative Templates When Possible
If a standard attribute or binding works, use it instead of a directive.
## Decide Between Directives and Components
Use a directive for DOM-level behavior. Use a component when behavior affects structure, state, or rendering.

View File

@@ -0,0 +1,159 @@
---
title: Avoid Excessive Component Abstraction in Large Lists
impact: MEDIUM
impactDescription: Each component instance has memory and render overhead - abstractions multiply this in lists
type: efficiency
tags: [vue3, performance, components, abstraction, lists, optimization]
---
# Avoid Excessive Component Abstraction in Large Lists
**Impact: MEDIUM** - Component instances are more expensive than plain DOM nodes. While abstractions improve code organization, unnecessary nesting creates overhead. In large lists, this overhead multiplies - 100 items with 3 levels of abstraction means 300+ component instances instead of 100.
Don't avoid abstraction entirely, but be mindful of component depth in frequently-rendered elements like list items.
## Task List
- Review list item components for unnecessary wrapper components
- Consider flattening component hierarchies in hot paths
- Use native elements when a component adds no value
- Profile component counts using Vue DevTools
- Focus optimization efforts on the most-rendered components
**BAD:**
```vue
<!-- BAD: Deep abstraction in list items -->
<template>
<div class="user-list">
<!-- For 100 users: Creates 400 component instances -->
<UserCard v-for="user in users" :key="user.id" :user="user" />
</div>
</template>
<!-- UserCard.vue -->
<template>
<Card> <!-- Wrapper component #1 -->
<CardHeader> <!-- Wrapper component #2 -->
<UserAvatar :src="user.avatar" /> <!-- Wrapper component #3 -->
</CardHeader>
<CardBody> <!-- Wrapper component #4 -->
<Text>{{ user.name }}</Text>
</CardBody>
</Card>
</template>
<!-- Each UserCard creates: Card + CardHeader + CardBody + UserAvatar + Text
100 users = 500+ component instances -->
```
**GOOD:**
```vue
<!-- GOOD: Flattened structure in list items -->
<template>
<div class="user-list">
<!-- For 100 users: Creates 100 component instances -->
<UserCard v-for="user in users" :key="user.id" :user="user" />
</div>
</template>
<!-- UserCard.vue - Flattened, uses native elements -->
<template>
<div class="card">
<div class="card-header">
<img :src="user.avatar" :alt="user.name" class="avatar" />
</div>
<div class="card-body">
<span class="user-name">{{ user.name }}</span>
</div>
</div>
</template>
<script setup>
defineProps({
user: Object
})
</script>
<style scoped>
/* Styles that would have been in Card, CardHeader, etc. */
.card { /* ... */ }
.card-header { /* ... */ }
.card-body { /* ... */ }
.avatar { /* ... */ }
</style>
```
## When Abstraction Is Still Worth It
```vue
<!-- Component abstraction is valuable when: -->
<!-- 1. Complex behavior is encapsulated -->
<UserStatusIndicator :user="user" /> <!-- Has logic, tooltips, etc. -->
<!-- 2. Reused outside of the hot path -->
<Card> <!-- OK to use in one-off places, not in 100-item lists -->
<!-- 3. The list itself is small -->
<template v-if="items.length < 20">
<FancyItem v-for="item in items" :key="item.id" />
</template>
<!-- 4. Virtualization is used (only ~20 items rendered at once) -->
<RecycleScroller :items="items">
<template #default="{ item }">
<ComplexItem :item="item" /> <!-- OK - only 20 instances exist -->
</template>
</RecycleScroller>
```
## Measuring Component Overhead
```javascript
// In development, profile component counts
import { onMounted, getCurrentInstance } from 'vue'
onMounted(() => {
const instance = getCurrentInstance()
let count = 0
function countComponents(vnode) {
if (vnode.component) count++
if (vnode.children) {
vnode.children.forEach(child => {
if (child.component || child.children) countComponents(child)
})
}
}
// Use Vue DevTools instead for accurate counts
console.log('Check Vue DevTools Components tab for instance counts')
})
```
## Alternatives to Wrapper Components
```vue
<!-- Instead of a <Button> component for styling: -->
<button class="btn btn-primary">Click</button>
<!-- Instead of a <Text> component: -->
<span class="text-body">{{ content }}</span>
<!-- Instead of layout wrapper components in lists: -->
<div class="flex items-center gap-2">
<!-- content -->
</div>
<!-- Use CSS classes or Tailwind instead of component abstractions for styling -->
```
## Impact Calculation
| List Size | Components per Item | Total Instances | Memory Impact |
|-----------|---------------------|-----------------|---------------|
| 100 items | 1 (flat) | 100 | Baseline |
| 100 items | 3 (nested) | 300 | ~3x memory |
| 100 items | 5 (deeply nested) | 500 | ~5x memory |
| 1000 items | 1 (flat) | 1000 | High |
| 1000 items | 5 (deeply nested) | 5000 | Very High |

View File

@@ -0,0 +1,182 @@
---
title: Use v-once and v-memo to Skip Unnecessary Updates
impact: MEDIUM
impactDescription: v-once skips all future updates for static content; v-memo conditionally memoizes subtrees
type: efficiency
tags: [vue3, performance, v-once, v-memo, optimization, directives]
---
# Use v-once and v-memo to Skip Unnecessary Updates
**Impact: MEDIUM** - Vue re-evaluates templates on every reactive change. For content that never changes or changes infrequently, `v-once` and `v-memo` tell Vue to skip updates, reducing render work.
Use `v-once` for truly static content and `v-memo` for conditionally-static content in lists.
## Task List
- Apply `v-once` to elements that use runtime data but never need updating
- Apply `v-memo` to list items that should only update on specific condition changes
- Verify memoized content doesn't need to respond to other state changes
- Profile with Vue DevTools to confirm update skipping
## v-once: Render Once, Never Update
**BAD:**
```vue
<template>
<!-- BAD: Re-evaluated on every parent re-render -->
<div class="terms-content">
<h1>Terms of Service</h1>
<p>Version: {{ termsVersion }}</p>
<div v-html="termsContent"></div>
</div>
<!-- This content NEVER changes, but Vue checks it every render -->
<footer>
<p>Copyright {{ copyrightYear }} {{ companyName }}</p>
</footer>
</template>
```
**GOOD:**
```vue
<template>
<!-- GOOD: Rendered once, skipped on all future updates -->
<div class="terms-content" v-once>
<h1>Terms of Service</h1>
<p>Version: {{ termsVersion }}</p>
<div v-html="termsContent"></div>
</div>
<!-- v-once tells Vue this never needs to update -->
<footer v-once>
<p>Copyright {{ copyrightYear }} {{ companyName }}</p>
</footer>
</template>
<script setup>
// These values are set once at component creation
const termsVersion = '2.1'
const termsContent = fetchedTermsHTML
const copyrightYear = 2024
const companyName = 'Acme Corp'
</script>
```
## v-memo: Conditional Memoization for Lists
**BAD:**
```vue
<template>
<!-- BAD: All items re-render when selectedId changes -->
<div v-for="item in list" :key="item.id">
<div :class="{ selected: item.id === selectedId }">
<ExpensiveComponent :data="item" />
</div>
</div>
</template>
```
**GOOD:**
```vue
<template>
<!-- GOOD: Items only re-render when their selection state changes -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id === selectedId]"
>
<div :class="{ selected: item.id === selectedId }">
<ExpensiveComponent :data="item" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([/* many items */])
const selectedId = ref(null)
// When selectedId changes:
// - Only the previously-selected item re-renders (selected: true -> false)
// - Only the newly-selected item re-renders (selected: false -> true)
// - All other items are SKIPPED (v-memo values unchanged)
</script>
```
## v-memo with Multiple Dependencies
```vue
<template>
<!-- Re-render only when item's selection OR editing state changes -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id === selectedId, item.id === editingId]"
>
<ItemCard
:item="item"
:selected="item.id === selectedId"
:editing="item.id === editingId"
/>
</div>
</template>
<script setup>
const selectedId = ref(null)
const editingId = ref(null)
const items = ref([/* ... */])
</script>
```
## v-memo with Empty Array = v-once
```vue
<template>
<!-- v-memo="[]" is equivalent to v-once -->
<div v-for="item in staticList" :key="item.id" v-memo="[]">
{{ item.name }}
</div>
</template>
```
## When NOT to Use These Directives
```vue
<template>
<!-- DON'T: Content that DOES need to update -->
<div v-once>
<span>Count: {{ count }}</span> <!-- count won't update! -->
</div>
<!-- DON'T: When child components have their own reactive state -->
<div v-memo="[selected]">
<InputField v-model="item.name" /> <!-- v-model won't work properly -->
</div>
<!-- DON'T: When the memoization benefit is minimal -->
<span v-once>{{ simpleText }}</span> <!-- Overhead not worth it -->
</template>
```
## Performance Comparison
| Scenario | Without Directive | With v-once/v-memo |
|----------|-------------------|-------------------|
| Static header, parent re-renders 100x | Re-evaluated 100x | Evaluated 1x |
| 1000 items, selection changes | 1000 items re-render | 2 items re-render |
| Complex child component | Full re-render | Skipped if memoized |
## Debugging Memoized Components
```vue
<script setup>
import { onUpdated } from 'vue'
// This won't fire if v-memo prevents update
onUpdated(() => {
console.log('Component updated')
})
</script>
```

View File

@@ -0,0 +1,187 @@
---
title: Virtualize Large Lists to Avoid DOM Overload
impact: HIGH
impactDescription: Rendering thousands of list items creates excessive DOM nodes, causing slow renders and high memory usage
type: efficiency
tags: [vue3, performance, virtual-list, large-data, dom, optimization]
---
# Virtualize Large Lists to Avoid DOM Overload
**Impact: HIGH** - Rendering all items in a large list (hundreds or thousands) creates massive amounts of DOM nodes. Each node consumes memory, slows down initial render, and makes updates expensive. List virtualization only renders visible items, dramatically improving performance.
Use a virtualization library when dealing with lists that could exceed 50-100 items, especially if items have complex content.
## Task List
- Identify lists that render more than 50-100 items
- Install a virtualization library (vue-virtual-scroller, @tanstack/vue-virtual)
- Replace standard `v-for` with virtualized component
- Ensure list items have consistent or estimable heights
- Test with realistic data volumes during development
## Recommended Libraries
| Library | Best For | Notes |
|---------|----------|-------|
| `vue-virtual-scroller` | General use, easy setup | Most popular, good defaults |
| `@tanstack/vue-virtual` | Complex layouts, headless | Framework-agnostic, flexible |
| `vue-virtual-scroll-grid` | Grid layouts | 2D virtualization |
| `vueuc/VVirtualList` | Naive UI projects | Part of Naive UI ecosystem |
**BAD:**
```vue
<template>
<!-- BAD: Renders ALL 10,000 items immediately -->
<div class="user-list">
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import UserCard from './UserCard.vue'
const users = ref([])
onMounted(async () => {
// 10,000 DOM nodes created, browser struggles
users.value = await fetchAllUsers()
})
</script>
```
**GOOD:**
```vue
<template>
<!-- GOOD: Only renders ~20 visible items at a time -->
<RecycleScroller
class="user-list"
:items="users"
:item-size="80"
key-field="id"
v-slot="{ item }"
>
<UserCard :user="item" />
</RecycleScroller>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import UserCard from './UserCard.vue'
const users = ref([])
onMounted(async () => {
// 10,000 items in memory, but only ~20 DOM nodes
users.value = await fetchAllUsers()
})
</script>
<style scoped>
.user-list {
height: 600px; /* Container must have fixed height */
}
</style>
```
## Using @tanstack/vue-virtual
```vue
<template>
<div ref="parentRef" class="list-container">
<div
:style="{
height: `${rowVirtualizer.getTotalSize()}px`,
position: 'relative'
}"
>
<div
v-for="virtualRow in rowVirtualizer.getVirtualItems()"
:key="virtualRow.key"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}"
>
<UserCard :user="users[virtualRow.index]" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const users = ref([/* 10,000 users */])
const parentRef = ref(null)
const rowVirtualizer = useVirtualizer({
count: users.value.length,
getScrollElement: () => parentRef.value,
estimateSize: () => 80, // Estimated row height
overscan: 5 // Render 5 extra items above/below viewport
})
</script>
<style scoped>
.list-container {
height: 600px;
overflow: auto;
}
</style>
```
## Dynamic Heights with vue-virtual-scroller
```vue
<template>
<!-- For variable height items, use DynamicScroller -->
<DynamicScroller
:items="messages"
:min-item-size="54"
key-field="id"
>
<template #default="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:data-index="index"
>
<ChatMessage :message="item" />
</DynamicScrollerItem>
</template>
</DynamicScroller>
</template>
<script setup>
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
</script>
```
## Performance Comparison
| Approach | 100 Items | 1,000 Items | 10,000 Items |
|----------|-----------|-------------|--------------|
| Regular v-for | ~100 DOM nodes | ~1,000 DOM nodes | ~10,000 DOM nodes |
| Virtualized | ~20 DOM nodes | ~20 DOM nodes | ~20 DOM nodes |
| Initial render | Fast | Slow | Very slow / crashes |
| Virtualized render | Fast | Fast | Fast |
## When NOT to Virtualize
- Lists under 50 items with simple content
- Lists where all items must be accessible to screen readers simultaneously
- Print layouts where all content must render
- SEO-critical content that must be in initial HTML

View File

@@ -0,0 +1,166 @@
---
title: Vue Plugin Best Practices
impact: MEDIUM
impactDescription: Incorrect plugin structure or injection key strategy causes install failures, collisions, and unsafe APIs
type: best-practice
tags: [vue3, plugins, provide-inject, typescript, dependency-injection]
---
# Vue Plugin Best Practices
**Impact: MEDIUM** - Vue plugins should follow the `app.use()` contract, expose explicit capabilities, and use collision-safe injection keys. This keeps plugin setup predictable and composable across large apps.
## Task List
- Export plugins as an object with `install()` or as an install function
- Use the `app` instance in `install()` to register components/directives/provides
- Type plugin APIs with `Plugin` (and options tuple types when needed)
- Use symbol keys (prefer `InjectionKey<T>`) for `provide/inject` in plugins
- Add a small typed composable wrapper for required injections to fail fast
## Structure Plugins for `app.use()`
A Vue plugin must be either:
- An object with `install(app, options?)`
- A function with the same signature
**BAD:**
```ts
const notAPlugin = {
doSomething() {}
}
app.use(notAPlugin)
```
**GOOD:**
```ts
import type { App } from 'vue'
interface PluginOptions {
prefix?: string
debug?: boolean
}
const myPlugin = {
install(app: App, options: PluginOptions = {}) {
const { prefix = 'my', debug = false } = options
if (debug) {
console.log('Installing myPlugin with prefix:', prefix)
}
app.provide('myPlugin', { prefix })
}
}
app.use(myPlugin, { prefix: 'custom', debug: true })
```
**GOOD:**
```ts
import type { App } from 'vue'
function simplePlugin(app: App, options?: { message: string }) {
app.config.globalProperties.$greet = () => options?.message ?? 'Hello!'
}
app.use(simplePlugin, { message: 'Welcome!' })
```
## Register Capabilities Explicitly in `install()`
Inside `install()`, wire behavior through Vue application APIs:
- `app.component()` for global components
- `app.directive()` for global directives
- `app.provide()` for injectable services and config
- `app.config.globalProperties` for optional global helpers (sparingly)
**BAD:**
```ts
const uselessPlugin = {
install(app, options) {
const service = createService(options)
}
}
```
**GOOD:**
```ts
const usefulPlugin = {
install(app, options) {
const service = createService(options)
app.provide(serviceKey, service)
}
}
```
## Type Plugin Contracts
Use Vue's `Plugin` type to keep install signatures and options type-safe.
```ts
import type { App, Plugin } from 'vue'
interface MyOptions {
apiKey: string
}
const myPlugin: Plugin<[MyOptions]> = {
install(app: App, options: MyOptions) {
app.provide(apiKeyKey, options.apiKey)
}
}
```
## Use Symbol Injection Keys in Plugins
String keys can collide (`'http'`, `'config'`, `'i18n'`). Use symbol keys with `InjectionKey<T>` so injections are unique and typed.
**BAD:**
```ts
export default {
install(app) {
app.provide('http', axios)
app.provide('config', appConfig)
}
}
```
**GOOD:**
```ts
import type { InjectionKey } from 'vue'
import type { AxiosInstance } from 'axios'
interface AppConfig {
apiUrl: string
timeout: number
}
export const httpKey: InjectionKey<AxiosInstance> = Symbol('http')
export const configKey: InjectionKey<AppConfig> = Symbol('appConfig')
export default {
install(app) {
app.provide(httpKey, axios)
app.provide(configKey, { apiUrl: '/api', timeout: 5000 })
}
}
```
## Provide Required Injection Helpers
Wrap required injections in composables that throw clear setup errors.
```ts
import { inject } from 'vue'
import { authKey, type AuthService } from '@/injection-keys'
export function useAuth(): AuthService {
const auth = inject(authKey)
if (!auth) {
throw new Error('Auth plugin not installed. Did you forget app.use(authPlugin)?')
}
return auth
}
```

View File

@@ -0,0 +1,344 @@
---
title: Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch)
impact: MEDIUM
impactDescription: Clear reactivity choices keep state predictable and reduce unnecessary updates in Vue 3 apps
type: efficiency
tags: [vue3, reactivity, ref, reactive, shallowRef, computed, watch, watchEffect, external-state, best-practice]
---
# Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch)
**Impact: MEDIUM** - Choose the right reactive primitive first, derive with `computed`, and use watchers only for side effects.
This reference covers the core reactivity decisions for local state, external data, derived values, and effects.
## Task List
- Declare reactive state correctly
- Always use `shallowRef()` instead of `ref()` for primitive values
- Choose the correct reactive declaration method for objects/arrays/map/set
- Follow best practices for `reactive`
- Avoid destructuring from `reactive()` directly
- Watch correctly for `reactive`
- Follow best practices for `computed`
- Prefer `computed` over watcher-assigned derived refs
- Keep filtered/sorted derivations out of templates
- Use `computed` for reusable class/style logic
- Keep computed getters pure (no side effects) and put side effects in watchers
- Follow best practices for watchers
- Use `immediate: true` instead of duplicate initial calls
- Clean up async effects for watchers
## Declare reactive state correctly
### Always use `shallowRef()` instead of `ref()` for primitive values (string, number, boolean, null, etc.) for better performance.
**Incorrect:**
```ts
import { ref } from 'vue'
const count = ref(0)
```
**Correct:**
```ts
import { shallowRef } from 'vue'
const count = shallowRef(0)
```
### Choose the correct reactive declaration method for objects/arrays/map/set
Use `ref()` when you often **replace the entire value** (`state.value = newObj`) and still want deep reactivity inside it, usually used for:
- Frequently reassigned state (replace fetched object/list, reset to defaults, switch presets).
- Composable return values where updates happen mostly via `.value` reassignment.
Use `reactive()` when you mainly **mutate properties** and full replacement is uncommon, usually used for:
- “Single state object” patterns (stores/forms): `state.count++`, `state.items.push(...)`, `state.user.name = ...`.
- Situations where you want to avoid `.value` and update nested fields in place.
```ts
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: 'Alice', age: 30 }
})
state.count++ // ✅ reactive
state.user.age = 31 // ✅ reactive
// ❌ avoid replacing the reactive object reference:
// state = reactive({ count: 1 })
```
Use `shallowRef()` when the value is **opaque / should not be proxied** (class instances, external library objects, very large nested data) and you only want updates to trigger when you **replace** `state.value` (no deep tracking), usually used for:
- Storing external instances/handles (SDK clients, class instances) without Vue proxying internals.
- Large data where you update by replacing the root reference (immutable-style updates).
```ts
import { shallowRef } from 'vue'
const user = shallowRef({ name: 'Alice', age: 30 })
user.value.age = 31 // ❌ not reactive
user.value = { name: 'Bob', age: 25 } // ✅ triggers update
```
Use `shallowReactive()` when you want **only top-level properties** reactive; nested objects remain raw, usually used for:
- Container objects where only top-level keys change and nested payloads should stay unmanaged/unproxied.
- Mixed structures where Vue tracks the wrapper object, but not deeply nested or foreign objects.
```ts
import { shallowReactive } from 'vue'
const state = shallowReactive({
count: 0,
user: { name: 'Alice', age: 30 }
})
state.count++ // ✅ reactive
state.user.age = 31 // ❌ not reactive
```
## Best practices for `reactive`
### Avoid destructuring from `reactive()` directly
**BAD:**
```ts
import { reactive } from 'vue'
const state = reactive({ count: 0 })
const { count } = state // ❌ disconnected from reactivity
```
### Watch correctly for reactive
**BAD:**
passing a non-getter value into `watch()`
```ts
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
// ❌ watch expects a getter, ref, reactive object, or array of these
watch(state.count, () => { /* ... */ })
```
**GOOD:**
preserve reactivity with `toRefs()` and use a getter for `watch()`
```ts
import { reactive, toRefs, watch } from 'vue'
const state = reactive({ count: 0 })
const { count } = toRefs(state) // ✅ count is a ref
watch(count, () => { /* ... */ }) // ✅
watch(() => state.count, () => { /* ... */ }) // ✅
```
## Best practices for `computed`
### Prefer `computed` over watcher-assigned derived refs
**BAD:**
```ts
import { ref, watchEffect } from 'vue'
const items = ref([{ price: 10 }, { price: 20 }])
const total = ref(0)
watchEffect(() => {
total.value = items.value.reduce((sum, item) => sum + item.price, 0)
})
```
**GOOD:**
```ts
import { ref, computed } from 'vue'
const items = ref([{ price: 10 }, { price: 20 }])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
```
### Keep filtered/sorted derivations out of templates
**BAD:**
```vue
<template>
<li v-for="item in items.filter(item => item.active)" :key="item.id">
{{ item.name }}
</li>
<li v-for="item in getSortedItems()" :key="item.id">
{{ item.name }}
</li>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'B', active: true },
{ id: 2, name: 'A', active: false }
])
function getSortedItems() {
return [...items.value].sort((a, b) => a.name.localeCompare(b.name))
}
</script>
```
**GOOD:**
```vue
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'B', active: true },
{ id: 2, name: 'A', active: false }
])
const visibleItems = computed(() =>
items.value
.filter(item => item.active)
.sort((a, b) => a.name.localeCompare(b.name))
)
</script>
<template>
<li v-for="item in visibleItems" :key="item.id">
{{ item.name }}
</li>
</template>
```
### Use `computed` for reusable class/style logic
**BAD:**
```vue
<template>
<button :class="{ btn: true, 'btn-primary': type === 'primary' && !disabled, 'btn-disabled': disabled }">
{{ label }}
</button>
</template>
```
**GOOD:**
```vue
<script setup>
import { computed } from 'vue'
const props = defineProps({
type: { type: String, default: 'primary' },
disabled: Boolean,
label: String
})
const buttonClasses = computed(() => ({
btn: true,
[`btn-${props.type}`]: !props.disabled,
'btn-disabled': props.disabled
}))
</script>
<template>
<button :class="buttonClasses">
{{ label }}
</button>
</template>
```
### Keep computed getters pure (no side effects) and put side effects in watchers instead
A computed getter should only derive a value. No mutation, no API calls, no storage writes, no event emits.
([Reference](https://vuejs.org/guide/essentials/computed.html#best-practices))
**BAD:**
side effects inside computed
```ts
const count = ref(0)
const doubled = computed(() => {
// ❌ side effect
if (count.value > 10) console.warn('Too big!')
return count.value * 2
})
```
**GOOD:**
pure computed + `watch()` for side effects
```ts
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (value) => {
if (value > 10) console.warn('Too big!')
})
```
## Best practices for watchers
### Use `immediate: true` instead of duplicate initial calls
**BAD:**
```ts
import { ref, watch, onMounted } from 'vue'
const userId = ref(1)
function loadUser(id) {
// ...
}
onMounted(() => loadUser(userId.value))
watch(userId, (id) => loadUser(id))
```
**GOOD:**
```ts
import { ref, watch } from 'vue'
const userId = ref(1)
watch(
userId,
(id) => loadUser(id),
{ immediate: true }
)
```
### Clean up async effects for watchers
When reacting to rapid changes (search boxes, filters), cancel the previous request.
**GOOD:**
```ts
const query = ref('')
const results = ref<string[]>([])
watch(query, async (q, _prev, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
})
results.value = await res.json()
})
```

View File

@@ -0,0 +1,201 @@
---
title: Render Function Patterns and Performance
impact: MEDIUM
impactDescription: Render functions require explicit patterns for lists, events, v-model, and performance to stay correct and maintainable
type: best-practice
tags: [vue3, render-function, h, v-model, directives, performance, jsx]
---
# Render Function Patterns and Performance
**Impact: MEDIUM** - Render functions are powerful but opt out of template compiler optimizations. Use them intentionally and apply the key patterns below to keep output correct and performant.
## Task List
- Prefer templates; use render functions only when templates cannot express the logic
- Always add stable keys when rendering lists with `h()`/JSX
- Use `withModifiers` / `withKeys` for event modifiers
- Implement `v-model` via `modelValue` + `onUpdate:modelValue`
- Apply custom directives with `withDirectives`
- Use functional components for stateless presentational UI
## Prefer templates over render functions
**BAD:**
```vue
<script setup>
import { h, ref } from 'vue'
const count = ref(0)
const render = () => h('div', `Count: ${count.value}`)
</script>
```
**GOOD:**
```vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>Count: {{ count }}</div>
</template>
```
## Always add keys for list rendering
**BAD:**
```javascript
import { h, ref } from 'vue'
export default {
setup() {
const items = ref([{ id: 1, name: 'Apple' }])
return () => h('ul',
items.value.map(item => h('li', item.name))
)
}
}
```
**GOOD:**
```javascript
import { h, ref } from 'vue'
export default {
setup() {
const items = ref([{ id: 1, name: 'Apple' }])
return () => h('ul',
items.value.map(item => h('li', { key: item.id }, item.name))
)
}
}
```
## Use `withModifiers` / `withKeys` for event modifiers
**BAD:**
```javascript
import { h } from 'vue'
export default {
setup() {
const handleClick = (e) => {
e.stopPropagation()
e.preventDefault()
}
return () => h('button', { onClick: handleClick }, 'Click')
}
}
```
**GOOD:**
```javascript
import { h, withModifiers, withKeys } from 'vue'
export default {
setup() {
const handleClick = () => {}
const handleEnter = () => {}
return () => h('div', [
h('button', {
onClick: withModifiers(handleClick, ['stop', 'prevent'])
}, 'Click'),
h('input', {
onKeyup: withKeys(handleEnter, ['enter'])
})
])
}
}
```
## Implement `v-model` explicitly
**BAD:**
```javascript
import { h, ref } from 'vue'
import CustomInput from './CustomInput.vue'
export default {
setup() {
const text = ref('')
return () => h(CustomInput, { modelValue: text.value })
}
}
```
**GOOD:**
```javascript
import { h, ref } from 'vue'
import CustomInput from './CustomInput.vue'
export default {
setup() {
const text = ref('')
return () => h(CustomInput, {
modelValue: text.value,
'onUpdate:modelValue': (value) => { text.value = value }
})
}
}
```
## Use `withDirectives` for custom directives
**BAD:**
```javascript
import { h } from 'vue'
const vFocus = { mounted: (el) => el.focus() }
export default {
setup() {
return () => h('input', { 'v-focus': true })
}
}
```
**GOOD:**
```javascript
import { h, withDirectives } from 'vue'
const vFocus = { mounted: (el) => el.focus() }
export default {
setup() {
return () => withDirectives(h('input'), [[vFocus]])
}
}
```
## Prefer functional components for stateless UI
**BAD:**
```javascript
import { h } from 'vue'
export default {
setup() {
return () => h('span', { class: 'badge' }, 'New')
}
}
```
**GOOD:**
```javascript
import { h } from 'vue'
function Badge(props, { slots }) {
return h('span', { class: 'badge' }, slots.default?.())
}
Badge.props = ['variant']
export default Badge
```

View File

@@ -0,0 +1,310 @@
---
title: Single-File Component Structure, Styling, and Template Patterns
impact: MEDIUM
impactDescription: Consistent SFC structure and styling choices improve maintainability, tooling support, and render performance
type: best-practice
tags: [vue3, sfc, scoped-css, styles, build-tools, performance, template, v-html, v-for, computed, v-if, v-show]
---
# Single-File Component Structure, Styling, and Template Patterns
**Impact: MEDIUM** - Using SFCs with consistent structure and performant styling keeps components easier to maintain and avoids unnecessary render overhead.
## Task List
- Use `.vue` SFCs instead of separate `.js`/`.ts` and `.css` files for components
- Colocate template, script, and styles in the same SFC by default
- Use PascalCase for component names in templates and filenames
- Prefer component-scoped styles
- Prefer class selectors (not element selectors) in scoped CSS for performance
- Access DOM / component refs with `useTemplateRef()` in Vue 3.5+
- Use camelCase keys in `:style` bindings for consistency and IDE support
- Use `v-for` and `v-if` correctly
- Never use `v-html` with untrusted/user-provided content
- Choose `v-if` vs `v-show` based on toggle frequency and initial render cost
## Colocate template, script, and styles
**BAD:**
```
components/
├── UserCard.vue
├── UserCard.js
└── UserCard.css
```
**GOOD:**
```vue
<!-- components/UserCard.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
user: { type: Object, required: true }
})
const displayName = computed(() =>
`${props.user.firstName} ${props.user.lastName}`
)
</script>
<template>
<div class="user-card">
<h3 class="name">{{ displayName }}</h3>
</div>
</template>
<style scoped>
.user-card {
padding: 1rem;
}
.name {
margin: 0;
}
</style>
```
## Use PascalCase for component names
**BAD:**
```vue
<script setup>
import userProfile from './user-profile.vue'
</script>
<template>
<user-profile :user="currentUser" />
</template>
```
**GOOD:**
```vue
<script setup>
import UserProfile from './UserProfile.vue'
</script>
<template>
<UserProfile :user="currentUser" />
</template>
```
## Best practices for `<style>` block in SFCs
### Prefer component-scoped styles
- Use `<style scoped>` for styles that belong to a component.
- Keep **global CSS** in a dedicated file (e.g. `src/assets/main.css`) for resets, typography, tokens, etc.
- Use `:deep()` sparingly (edge cases only).
**BAD:**
```vue
<style>
/* ❌ leaks everywhere */
button { border-radius: 999px; }
</style>
```
**GOOD:**
```vue
<style scoped>
.button { border-radius: 999px; }
</style>
```
**GOOD:**
```css
/* src/assets/main.css */
/* ✅ resets, tokens, typography, app-wide rules */
:root { --radius: 999px; }
```
### Use class selectors in scoped CSS
**BAD:**
```vue
<template>
<article>
<h1>{{ title }}</h1>
<p>{{ subtitle }}</p>
</article>
</template>
<style scoped>
article { max-width: 800px; }
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
```
**GOOD:**
```vue
<template>
<article class="article">
<h1 class="article-title">{{ title }}</h1>
<p class="article-subtitle">{{ subtitle }}</p>
</article>
</template>
<style scoped>
.article { max-width: 800px; }
.article-title { font-size: 2rem; }
.article-subtitle { line-height: 1.6; }
</style>
```
## Access DOM / component refs with `useTemplateRef()`
For Vue 3.5+: use `useTemplateRef()` to access template refs.
```vue
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>
```
## Use camelCase in `:style` bindings
**BAD:**
```vue
<template>
<div :style="{ 'font-size': fontSize + 'px', 'background-color': bg }">
Content
</div>
</template>
```
**GOOD:**
```vue
<template>
<div :style="{ fontSize: fontSize + 'px', backgroundColor: bg }">
Content
</div>
</template>
```
## Use `v-for` and `v-if` correctly
### Always provide a stable `:key`
- Prefer primitive keys (`string | number`).
- Avoid using objects as keys.
**GOOD:**
```vue
<li v-for="item in items" :key="item.id">
<input v-model="item.text" />
</li>
```
### Avoid `v-if` and `v-for` on the same element
It leads to unclear intent and unnecessary work.
([Reference](https://vuejs.org/guide/essentials/list.html#v-for-with-v-if))
**To filter items**
**BAD:**
```vue
<li v-for="user in users" v-if="user.active" :key="user.id">
{{ user.name }}
</li>
```
**GOOD:**
```vue
<script setup lang="ts">
import { computed } from 'vue'
const activeUsers = computed(() => users.value.filter(u => u.active))
</script>
<template>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
```
**To conditionally show/hide the entire list**
**GOOD:**
```vue
<ul v-if="shouldShowUsers">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
```
## Never render untrusted HTML with `v-html`
**BAD:**
```vue
<template>
<!-- DANGEROUS: untrusted input can inject scripts -->
<article v-html="userProvidedContent"></article>
</template>
```
**GOOD:**
```vue
<script setup>
import { computed } from 'vue'
import DOMPurify from 'dompurify'
const props = defineProps<{
trustedHtml?: string
plainText: string
}>()
const safeHtml = computed(() => DOMPurify.sanitize(props.trustedHtml ?? ''))
</script>
<template>
<!-- Preferred: escaped interpolation -->
<p>{{ props.plainText }}</p>
<!-- Only for trusted/sanitized HTML -->
<article v-html="safeHtml"></article>
</template>
```
## Choose `v-if` vs `v-show` by toggle behavior
**BAD:**
```vue
<template>
<!-- Frequent toggles with v-if cause repeated mount/unmount -->
<ComplexPanel v-if="isPanelOpen" />
<!-- Rarely shown content with v-show pays initial render cost -->
<AdminPanel v-show="isAdmin" />
</template>
```
**GOOD:**
```vue
<template>
<!-- Frequent toggles: keep in DOM, toggle display -->
<ComplexPanel v-show="isPanelOpen" />
<!-- Rare condition: lazy render only when true -->
<AdminPanel v-if="isAdmin" />
</template>
```

View File

@@ -0,0 +1,135 @@
---
title: State Management Strategy
impact: HIGH
impactDescription: Choosing the wrong store pattern can cause SSR request leaks, brittle mutation flows, and poor scaling
type: best-practice
tags: [vue3, state-management, pinia, composables, ssr, vueuse]
---
# State Management Strategy
**Impact: HIGH** - Use the lightest state solution that fits your app architecture. SPA-only apps can use lightweight global composables, while SSR/Nuxt apps should default to Pinia for request-safe isolation and predictable tooling.
## Task List
- Keep state local first, then promote to shared/global only when needed
- Use singleton composables only in non-SSR applications
- Expose global state as readonly and mutate through explicit actions
- Prefer Pinia for SSR/Nuxt, large apps, and advanced debugging/plugin needs
- Avoid exporting mutable module-level reactive state directly
## Choose the Lightest Store Approach
- **Feature composable:** Default for reusable logic with local/feature-level state.
- **Singleton composable or VueUse `createGlobalState`:** Small non-SSR apps needing shared app state.
- **Pinia:** SSR/Nuxt apps, medium-to-large apps, and cases requiring DevTools, plugins, or action tracing.
## Avoid Exporting Mutable Module State
**BAD:**
```ts
// store/cart.ts
import { reactive } from 'vue'
export const cart = reactive({
items: [] as Array<{ id: string; qty: number }>
})
```
**GOOD:**
```ts
// composables/useCartStore.ts
import { reactive, readonly } from 'vue'
let _store: ReturnType<typeof createCartStore> | null = null
function createCartStore() {
const state = reactive({
items: [] as Array<{ id: string; qty: number }>
})
function addItem(id: string, qty = 1) {
const existing = state.items.find((item) => item.id === id)
if (existing) {
existing.qty += qty
return
}
state.items.push({ id, qty })
}
return {
state: readonly(state),
addItem
}
}
export function useCartStore() {
if (!_store) _store = createCartStore()
return _store
}
```
## Do Not Use Runtime Singletons in SSR
Module singletons live for the runtime lifetime. In SSR this can leak state between requests.
**BAD:**
```ts
// shared singleton reused across requests
const cartStore = useCartStore()
export function useServerCart() {
return cartStore
}
```
**GOOD:**
> `pinia` dependency required.
```ts
// stores/cart.ts
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as Array<{ id: string; qty: number }>
}),
actions: {
addItem(id: string, qty = 1) {
const existing = this.items.find((item) => item.id === id)
if (existing) {
existing.qty += qty
return
}
this.items.push({ id, qty })
}
}
})
```
## Use `createGlobalState` for Small SPA Global State
> `@vueuse/core` dependency required.
If the app is non-SSR and already uses VueUse, `createGlobalState` removes singleton boilerplate.
```ts
import { createGlobalState } from '@vueuse/core'
import { computed, ref } from 'vue'
export const useAuthState = createGlobalState(() => {
const token = ref<string | null>(null)
const isAuthenticated = computed(() => token.value !== null)
function setToken(next: string | null) {
token.value = next
}
return {
token,
isAuthenticated,
setToken
}
})
```

View File

@@ -0,0 +1,187 @@
---
title: Avoid Expensive Operations in Updated Hook
impact: MEDIUM
impactDescription: Heavy computations in updated hook cause performance bottlenecks and potential infinite loops
type: capability
tags: [vue3, vue2, lifecycle, updated, performance, optimization, reactivity]
---
# Avoid Expensive Operations in Updated Hook
**Impact: MEDIUM** - The `updated` hook runs after every reactive state change that causes a re-render. Placing expensive operations, API calls, or state mutations here can cause severe performance degradation, infinite loops, and dropped frames below the optimal 60fps threshold.
Use `updated`/`onUpdated` sparingly for post-DOM-update operations that cannot be handled by watchers or computed properties. For most reactive data handling, prefer watchers (`watch`/`watchEffect`) which provide more control over what triggers the callback.
## Task List
- Never perform API calls in updated hook
- Never mutate reactive state inside updated (causes infinite loops)
- Use conditional checks to verify updates are relevant before acting
- Prefer `watch` or `watchEffect` for reacting to specific data changes
- Use throttling/debouncing if updated operations are expensive
- Reserve updated for low-level DOM synchronization tasks
**BAD:**
```javascript
// BAD: API call in updated - fires on every re-render
export default {
data() {
return { items: [], lastUpdate: null }
},
updated() {
// This runs after every single state change!
fetch('/api/sync', {
method: 'POST',
body: JSON.stringify(this.items)
})
}
}
```
```javascript
// BAD: State mutation in updated - infinite loop
export default {
data() {
return { renderCount: 0 }
},
updated() {
// This causes another update, which triggers updated again!
this.renderCount++ // Infinite loop
}
}
```
```javascript
// BAD: Heavy computation on every update
export default {
updated() {
// Expensive operation runs on every keystroke, every state change
this.processedData = this.heavyComputation(this.rawData)
this.analytics = this.calculateMetrics(this.allData)
}
}
```
**GOOD:**
```javascript
import debounce from 'lodash-es/debounce'
// GOOD: Use watcher for specific data changes
export default {
data() {
return { items: [] }
},
watch: {
// Only fires when items actually changes
items: {
handler(newItems) {
this.syncToServer(newItems)
},
deep: true
}
},
methods: {
syncToServer: debounce(function(items) {
fetch('/api/sync', {
method: 'POST',
body: JSON.stringify(items)
})
}, 500)
}
}
```
```vue
<!-- GOOD: Composition API with targeted watchers -->
<script setup>
import { ref, watch, onUpdated } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const items = ref([])
const scrollContainer = ref(null)
// Watch specific data - not all updates
watch(items, (newItems) => {
syncToServer(newItems)
}, { deep: true })
const syncToServer = useDebounceFn((items) => {
fetch('/api/sync', { method: 'POST', body: JSON.stringify(items) })
}, 500)
// Only use onUpdated for DOM synchronization
onUpdated(() => {
// Scroll to bottom only if content changed height
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
}
})
</script>
```
```javascript
// GOOD: Conditional check in updated hook
export default {
data() {
return {
content: '',
lastSyncedContent: ''
}
},
updated() {
// Only act if specific condition is met
if (this.content !== this.lastSyncedContent) {
this.syncContent()
this.lastSyncedContent = this.content
}
},
methods: {
syncContent: debounce(function() {
// Sync logic
}, 300)
}
}
```
## Valid Use Cases for Updated Hook
```javascript
// GOOD: Low-level DOM synchronization
export default {
updated() {
// Sync third-party library with Vue's DOM
this.thirdPartyWidget.refresh()
// Update scroll position after content change
this.$nextTick(() => {
this.maintainScrollPosition()
})
}
}
```
## Prefer Computed Properties for Derived Data
```javascript
// BAD: Calculating derived data in updated
export default {
data() {
return { numbers: [1, 2, 3, 4, 5] }
},
updated() {
this.sum = this.numbers.reduce((a, b) => a + b, 0) // Causes another update!
}
}
// GOOD: Use computed property instead
export default {
data() {
return { numbers: [1, 2, 3, 4, 5] }
},
computed: {
sum() {
return this.numbers.reduce((a, b) => a + b, 0)
}
}
}
```