--- name: vue-dev description: "Generate modern, maintainable Vue TypeScript code with best practices: Composition API, script setup with TS, composable patterns over global stores, props destructure, VueUse integration, Tailwind CSS, accessibility, performance optimization, and Vitest testing. Use when creating Vue 3 components, composables, or refactoring Vue code to follow modern patterns." --- # Vue Dev - Modern Vue TypeScript Development ## Core Principles **Always follow these principles unless project context demands otherwise:** - **Composition API** with ` ``` ## Composable Patterns ### Composable Decision Tree **Create separate composable file when:** - Logic is reused in 2+ components - Logic involves async operations with loading/error states - Logic requires complex state management - Logic benefits from isolation and testing **Inline composable when:** - Logic is single-use and simple - Logic is tightly coupled to specific component - Logic is less than 5 lines ### Composable Template Structure ```typescript // useFeatureName.ts import { ref, computed, watch } from 'vue' import { useLocalStorage, useDebounceFn } from '@vueuse/core' import type { SomeType } from './types' /** * Composable for [brief description] * * @param param - Description of parameter * @returns Object with reactive properties and methods * * @example * ```ts * const { data, loading, error, refresh } = useFeatureName({ * id: '123', * autoFetch: true * }) * ``` */ export function useFeatureName(options: { id: string autoFetch?: boolean debounce?: number }) { // State const data = ref(null) const loading = ref(false) const error = ref(null) // Composables from VueUse const storage = useLocalStorage(`feature-${options.id}`, null) // Methods const fetchData = async (): Promise => { loading.value = true error.value = null try { const response = await fetch(`/api/data/${options.id}`) const result = await response.json() as T data.value = result } catch (err) { error.value = err instanceof Error ? err : new Error('Unknown error') } finally { loading.value = false } } const debouncedFetch = useDebounceFn(fetchData, options.debounce ?? 300) // Computed const hasData = computed(() => data.value !== null) const errorMessage = computed(() => error.value?.message ?? '') // Lifecycle if (options.autoFetch) { fetchData() } return { // State data, loading, error, // Computed hasData, errorMessage, // Methods refresh: debouncedFetch, } } ``` ### Async Wrapper Pattern For async operations, prefer wrapper functions that return data/error properties: ```typescript // useAsync.ts import { ref, type Ref } from 'vue' export type AsyncResult = { data: Ref loading: Ref error: Ref } /** * Wrapper for async operations with consistent error handling */ export function useAsync( fn: () => Promise, options: { immediate?: boolean } = {} ): AsyncResult { const data = ref(null) as Ref const loading = ref(false) const error = ref(null) const execute = async (): Promise => { loading.value = true error.value = null try { data.value = await fn() } catch (err) { error.value = err instanceof Error ? err : new Error('Unknown error') } finally { loading.value = false } } if (options.immediate) { execute() } return { data, loading, error, } } ``` ## State Management Patterns ### Nuxt: useState Pattern ```typescript // composables/useSharedState.ts export const useCounter = () => { const counter = useState('counter', () => 0) const increment = () => { counter.value++ } return { counter: readonly(counter), increment, } } ``` ### Vite: Global Ref Pattern ```typescript // store/globalStore.ts import { ref } from 'vue' const globalState = ref(0) export function useGlobalState() { const increment = () => { globalState.value++ } return { counter: readonly(globalState), increment, } } ``` ### Provider Pattern (Vite) ```typescript // context/UserContext.ts import { provide, inject, type InjectionKey, readonly } from 'vue' import type { User } from './types' const UserContextKey: InjectionKey> = Symbol('User') export function provideUserContext(user: User) { const state = useUserState(user) provide(UserContextKey, state) } export function useUserContext() { const context = inject(UserContextKey) if (!context) { throw new Error('useUserContext must be used within provideUserContext') } return context } function useUserState(initialUser: User) { const currentUser = ref(initialUser) const setUser = (user: User) => { currentUser.value = user } return { currentUser: readonly(currentUser), setUser, } } ``` ## VueUsage Patterns Always check VueUse first. Common patterns: ### Common VueUse Functions ```typescript import { // Storage useLocalStorage, useSessionStorage, useStorage, // DOM onClickOutside, onKeyStroke, useElementBounding, useWindowSize, useElementSize, useIntersectionObserver, // Async useAsyncState, useFetch, // Utilities useDebounceFn, useThrottleFn, useToggle, useClipboard, useTitle, // Sensors useMouse, useScroll, useMediaQuery, useNetwork, // Formatters useDateFormat, useTimeAgo, } from '@vueuse/core' ``` ### Examples ```typescript // Close dropdown when clicking outside const buttonRef = ref() onClickOutside(buttonRef, () => isOpen.value = false) // Responsive design const isMobile = useMediaQuery('(max-width: 768px)') // Debounced search const debouncedSearch = useDebounceFn((query: string) => { // search logic }, 300) // Local storage persistence const theme = useLocalStorage('theme', 'light') ``` ## TypeScript Patterns ### Type Definition Use `type` aliases over interfaces: ```typescript // ✅ Good type User = { id: string name: string email: string } // ❌ Avoid interface User { id: string name: string email: string } ``` ### Props Typing ```typescript // Simple props defineProps<{ label: string count: number }>() // Optional props with defaults const { count = 0 } = defineProps<{ label: string count?: number }>() // Generic props defineProps<{ items: Array<{ id: string; name: string }> selected?: string }>() ``` ### Emit Typing ```typescript const emit = defineEmits<{ change: [id: number] update: [value: string] }>() ``` ### Ref Typing ```typescript // Explicit type const count = ref(0) // Inferred from initial value const message = ref('hello') // Nullable ref const user = ref(null) // Array ref const items = ref([]) ``` ## Performance Best Practices ### Computed vs Watch ```typescript // ✅ Use computed for derived state const fullName = computed(() => `${firstName.value} ${lastName.value}`) // ✅ Use watch for side effects watch(fullName, (newName) => { console.log('Name changed:', newName) }) ``` ### Shallow Refs for Large Objects ```typescript // For large objects or frequent reassignments const largeData = shallowRef([]) ``` ### Lazy Loading Components ```typescript const Modal = defineAsyncComponent(() => import('./Modal.vue')) ``` ### v-once for Static Content ```vue ``` ### v-memo for Expensive Computations ```vue ``` ## Accessibility Guidelines ### Semantic HTML ```vue ``` ### ARIA Attributes ```vue ``` ### Keyboard Navigation ```vue ``` ### Form Accessibility ```vue ``` ## Testing Patterns ### Composable Testing ```typescript // tests/useFeatureName.spec.ts import { describe, it, expect, beforeEach } from 'vitest' import { ref } from 'vue' import { useFeatureName } from '../useFeatureName' describe('useFeatureName', () => { it('should initialize with default values', () => { const { data, loading, error } = useFeatureName({ id: 'test', autoFetch: false, }) expect(data.value).toBeNull() expect(loading.value).toBe(false) expect(error.value).toBeNull() }) it('should fetch data successfully', async () => { const { data, refresh } = useFeatureName({ id: 'test', autoFetch: false, }) await refresh() expect(data.value).toBeDefined() }) }) ``` ## File Organization **Always use flat structure:** ``` src/ ├── components/ │ ├── Button.vue │ ├── Input.vue │ └── Modal.vue ├── composables/ │ ├── useAuth.ts │ ├── useFetch.ts │ └── useForm.ts ├── types/ │ ├── user.ts │ └── api.ts └── utils/ └── format.ts ``` ## Code Quality Checklist Before finalizing any component or composable: - [ ] TypeScript strict mode compatible - [ ] Props properly typed with `defineProps()` - [ ] Props destructured for defaults - [ ] Emits typed with `defineEmits()` - [ ] VueUsed checked and applied where applicable - [ ] Tailwind classes used (no `