10 KiB
description
| description |
|---|
| Load Vue component patterns and best practices |
Vue Component Priming
Note: This command references the
nuxt:nuxtskill for progressive disclosure of additional Vue patterns and library-specific documentation.
Script Setup Syntax
ALWAYS use <script setup lang="ts"> for component script sections.
Props
ALWAYS use TypeScript type-based syntax for defineProps():
// ✅ Correct: Type-based with destructuring and inline defaults
const {
title,
count = 0,
enabled = true,
} = defineProps<{
title: string
count?: number
enabled?: boolean
}>()
// ✅ Correct: No props used in script
defineProps<{
title: string
}>()
// ❌ Wrong: Runtime PropType syntax
import type { PropType } from "vue"
defineProps({
items: {
type: Array as PropType<string[]>,
required: true,
},
})
Emits
ALWAYS use type-based syntax for defineEmits:
// ✅ Correct: Type-based emits
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// ❌ Wrong: Runtime array syntax
const emit = defineEmits(["update", "close"])
Event Handler Typing
When emitting events with event objects, use appropriate event types:
// ✅ Correct: Typed event handlers
const emit = defineEmits<{
click: [event: MouseEvent]
keypress: [event: KeyboardEvent]
input: [event: InputEvent]
submit: [event: SubmitEvent]
}>()
// Usage in template
<button @click="emit('click', $event)">Click me</button>
<input @keypress="emit('keypress', $event)" />
v-model
USE defineModel() for v-model implementations:
// ✅ Correct: Using defineModel
const modelValue = defineModel<string>()
// With options
const modelValue = defineModel<string>({ required: true })
// ❌ Wrong: Manual prop + emit
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ "update:modelValue": [value: string] }>()
Component Structure
Template Placement
ALWAYS place <template> section first, before <script> and <style>:
<!-- ✅ Correct order -->
<template>
<div>{{ title }}</div>
</template>
<script setup lang="ts">
const { title } = defineProps<{ title: string }>()
</script>
<style scoped>
div {
color: blue;
}
</style>
Component Naming
- ALWAYS use multi-word component names except for Nuxt pages and layouts
- Examples:
UserProfile.vue,SearchBar.vue(notUser.vue,Search.vue) - Exception:
pages/index.vue,pages/about.vue,layouts/default.vue
Template Directives
v-for Loops
ALWAYS use key in v-for loops and prefer of over in:
<!-- ✅ Correct -->
<li v-for="user of users" :key="user.id">{{ user.name }}</li>
<!-- ❌ Wrong: Missing key -->
<li v-for="user of users">{{ user.name }}</li>
<!-- ❌ Wrong: Using 'in' instead of 'of' -->
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
Prop Binding Shorthand
ALWAYS use shorthand syntax when passing props with same name as variable:
<!-- ✅ Correct: Shorthand -->
<UserCard :username :avatar :bio />
<!-- ❌ Wrong: Verbose when unnecessary -->
<UserCard :username="username" :avatar="avatar" :bio="bio" />
Reactivity and State
Reactive References
PREFER ref() for reactive state instead of reactive():
// ✅ Preferred: Using ref
const count = ref(0)
const user = ref({ name: "Alice", age: 30 })
// ❌ Less preferred: Using reactive (loses reactivity on destructure)
const state = reactive({ count: 0 })
Auto-Imported Vue APIs
Never manually import these in Nuxt projects - they're auto-imported:
Reactivity:
ref- Reactive primitive valuesreactive- Reactive objectscomputed- Computed valueswatch- Watch reactive values
Lifecycle:
onMounted- Component mountedonUnmounted- Component unmountedonBeforeMount,onBeforeUnmount, etc.
Component APIs:
defineProps- Define props (type-based)defineEmits- Define emits (type-based)defineModel- Define v-model (type-based)
Utilities:
useId- Generate unique IDs for accessibility/form elements (SSR-safe)
Component Organization
Logical Grouping
PREFER to group by logical concerns rather than by type:
// ✅ Preferred: Grouped by feature/concern
<script setup lang="ts">
// User authentication concern
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
async function login() { /* ... */ }
// Search functionality concern
const searchQuery = ref('')
const searchResults = computed(() => /* ... */)
function handleSearch() { /* ... */ }
</script>
// ❌ Less preferred: Grouped by type
<script setup lang="ts">
// All refs
const user = ref(null)
const searchQuery = ref('')
// All computed
const isAuthenticated = computed(() => !!user.value)
const searchResults = computed(() => /* ... */)
// All functions
async function login() { /* ... */ }
function handleSearch() { /* ... */ }
</script>
Styling Strategy
Check package.json for @nuxtjs/tailwindcss:
If Tailwind Installed
<template>
<div class="flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-800">
<!-- Use Tailwind utilities -->
<!-- Arbitrary variants: [&::-webkit-scrollbar]:w-1.5 -->
<!-- Custom properties: @theme directive -->
</div>
</template>
If NO Tailwind
ALWAYS use <style scoped>. PREFER short, simple class names - scoped styles eliminate need for BEM:
<!-- ✅ Preferred -->
<template>
<div class="card">
<div class="header">Title</div>
<div class="content">Body</div>
</div>
</template>
<style scoped>
.card { padding: 1rem; }
.header { font-weight: bold; }
.content { flex: 1; }
</style>
<!-- ❌ Avoid: BEM with scoped styles -->
<div class="user-card">
<div class="user-card__header">Title</div>
</div>
VueUse Composables (If Installed)
Check package.json for @vueuse/core or @vueuse/nuxt:
PREFER VueUse composables over custom implementations for common tasks:
// ✅ Preferred: Using VueUse (if installed)
import { useLocalStorage, useMouse, useWindowSize } from "@vueuse/core"
const token = useLocalStorage("auth-token", "")
// ❌ Avoid: Custom implementation when VueUse exists
const token = ref(localStorage.getItem("auth-token") || "")
watch(token, (val) => localStorage.setItem("auth-token", val))
Common VueUse Patterns
State:
useToggle,useCounter,useLocalStorage,useSessionStorage
DOM:
useMouse,useScroll,useElementVisibility,useIntersectionObserver,useResizeObserver
Browser:
useClipboard,useMediaQuery,useDark,usePreferredDark,useGeolocation
Utilities:
refDebounced,useDebounceFn,refThrottled,useThrottleFn,useInterval,useTimeout
The nuxt:nuxt skill provides detailed VueUse reference when installed.
Accessibility
- Use semantic HTML:
<button>,<nav>,<main>,<article> - Add ARIA attributes to interactive elements
- Ensure keyboard navigation (tab order, enter/space handlers)
TypeScript Types
- Place component prop interfaces in same file or
/typesdirectory - Use PascalCase:
ButtonProps,CardProps,UserState - Never use
as any- prefer type guards oras unknown as Type
Performance Patterns
Computed vs Methods
Use computed() for derived state (cached):
// ✅ Cached, only recalculates when dependencies change
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ❌ Recalculates on every render
const getFullName = () => `${firstName.value} ${lastName.value}`
Static Content
Use v-once for static content that never changes:
<div v-once>
<h1>Static Header</h1>
<p>This content never changes</p>
</div>
Expensive Lists
Use v-memo for expensive lists with stable data:
<div v-for="item of list" :key="item.id" v-memo="[item.id]">
<!-- Expensive rendering -->
</div>
Template Best Practices
Conditional Rendering
- Use
v-showfor frequent toggles - Use
v-iffor conditional rendering
<!-- Frequent toggling: keep in DOM -->
<div v-show="isVisible">Toggle me often</div>
<!-- Conditional: add/remove from DOM -->
<div v-if="hasPermission">Render only when needed</div>
Event Handling
<!-- Inline handlers for simple cases -->
<button @click="count++">Increment</button>
<!-- Method refs for complex logic -->
<button @click="handleSubmit">Submit</button>
<!-- Modifiers -->
<button @click.prevent="handleClick">Prevent Default</button>
<input @keyup.enter="handleEnter" />
Slots
<!-- Basic slot -->
<template>
<div class="card">
<slot />
</div>
</template>
<!-- Named slots -->
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
</template>
<!-- Scoped slots -->
<template>
<ul>
<li v-for="item of items" :key="item.id">
<slot :item="item" />
</li>
</ul>
</template>
Provide/Inject
For dependency injection:
// Parent component
provide("theme", "dark")
provide("api", apiClient)
// Child component (any depth)
const theme = inject("theme")
const api = inject("api")
// With TypeScript
import type { InjectionKey } from "vue"
interface Theme {
mode: "light" | "dark"
}
const themeKey: InjectionKey<Theme> = Symbol("theme")
// Provide
provide(themeKey, { mode: "dark" })
// Inject
const theme = inject(themeKey)
Lifecycle Hooks
// Setup (reactive state initialization)
const count = ref(0)
// Mounted (DOM available)
onMounted(() => {
console.log("Component mounted")
})
// Before unmount (cleanup)
onBeforeUnmount(() => {
// Remove event listeners, clear timers, etc.
})
// Unmounted
onUnmounted(() => {
console.log("Component unmounted")
})
// Watch effect (runs immediately and on dependencies change)
watchEffect(() => {
console.log(`Count is ${count.value}`)
})
// Watch specific value
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
Template Refs
<template>
<input ref="inputRef" />
<MyComponent ref="componentRef" />
</template>
<script setup lang="ts">
const inputRef = ref<HTMLInputElement>()
const componentRef = ref<InstanceType<typeof MyComponent>>()
onMounted(() => {
inputRef.value?.focus()
})
</script>