Vue 3 Composition API

Master Vue 3 Composition API, reactivity, and modern Vue patterns

# Vue 3 Composition API Best Practices

Comprehensive guide for mastering Vue 3's Composition API, reactivity system, and building scalable Vue applications.

---

## Core Composition API Principles

1. **Setup Function and Script Setup**
   - Use `<script setup>` syntax for cleaner, more concise components
   - Leverage automatic component registration and prop inference
   - Understand the differences between setup() function and script setup
   - Example setup patterns:
     ```vue
     <!-- Preferred: Script Setup Syntax -->
     <script setup lang="ts">
     import { ref, computed, onMounted } from 'vue'
     import type { User } from '@/types'

     // Props are automatically inferred
     interface Props {
       userId: string
       initialData?: User
     }
     const props = defineProps<Props>()

     // Emits are automatically registered
     const emit = defineEmits<{
       update: [user: User]
       delete: [id: string]
     }>()

     // Reactive state
     const user = ref<User | null>(null)
     const loading = ref(false)

     // Computed values
     const displayName = computed(() =>
       user.value ? `${user.value.firstName} ${user.value.lastName}` : 'Unknown'
     )

     // Lifecycle hooks
     onMounted(async () => {
       if (props.initialData) {
         user.value = props.initialData
       } else {
         await fetchUser()
       }
     })

     // Methods
     const fetchUser = async () => {
       loading.value = true
       try {
         user.value = await api.getUser(props.userId)
       } finally {
         loading.value = false
       }
     }
     </script>
     ```

2. **Reactivity Fundamentals**
   - Use `ref()` for primitive values and single references
   - Use `reactive()` for objects and arrays
   - Understand reactive vs shallow reactive patterns
   - Example reactivity patterns:
     ```ts
     import { ref, reactive, computed, toRefs } from 'vue'

     // Primitives use ref
     const count = ref(0)
     const name = ref('John')
     const isVisible = ref(true)

     // Objects use reactive
     const user = reactive({
       id: 1,
       name: 'John Doe',
       email: 'john@example.com',
       preferences: {
         theme: 'dark',
         notifications: true
       }
     })

     // Computed based on reactive data
     const userDisplayInfo = computed(() => ({
       displayName: user.name,
       contactInfo: user.email,
       isActive: user.preferences.notifications
     }))

     // When destructuring reactive objects, use toRefs
     const { name: userName, email } = toRefs(user)
     ```

3. **TypeScript Integration**
   - Use proper TypeScript types throughout Vue components
   - Leverage Vue's built-in TypeScript support
   - Create type-safe composables and components
   - Example TypeScript patterns:
     ```ts
     import { ref, computed, type Ref, type ComputedRef } from 'vue'

     interface User {
       id: number
       name: string
       email: string
       role: 'admin' | 'user' | 'guest'
     }

     interface UseUserReturn {
       user: Ref<User | null>
       loading: Ref<boolean>
       error: Ref<string | null>
       fetchUser: (id: number) => Promise<void>
       updateUser: (updates: Partial<User>) => Promise<void>
       isAdmin: ComputedRef<boolean>
     }

     function useUser(initialId?: number): UseUserReturn {
       const user = ref<User | null>(null)
       const loading = ref(false)
       const error = ref<string | null>(null)

       const isAdmin = computed(() => user.value?.role === 'admin')

       const fetchUser = async (id: number) => {
         loading.value = true
         error.value = null
         try {
           user.value = await api.getUser(id)
         } catch (err) {
           error.value = err instanceof Error ? err.message : 'Unknown error'
         } finally {
           loading.value = false
         }
       }

       const updateUser = async (updates: Partial<User>) => {
         if (!user.value) return

         try {
           const updated = await api.updateUser(user.value.id, updates)
           user.value = updated
         } catch (err) {
           error.value = err instanceof Error ? err.message : 'Update failed'
         }
       }

       return {
         user,
         loading,
         error,
         fetchUser,
         updateUser,
         isAdmin
       }
     }
     ```

---

## Advanced Reactivity Patterns

4. **Computed Properties and Watchers**
   - Use computed for derived state that depends on reactive data
   - Implement watchers for side effects and complex reactions
   - Understand when to use watch vs watchEffect
   - Example advanced reactivity:
     ```ts
     import { ref, computed, watch, watchEffect } from 'vue'

     const searchQuery = ref('')
     const searchResults = ref([])
     const selectedFilters = reactive({
       category: 'all',
       priceRange: [0, 1000],
       inStock: true
     })

     // Computed for filtered and sorted results
     const filteredResults = computed(() => {
       return searchResults.value
         .filter(item => {
           if (selectedFilters.category !== 'all' && item.category !== selectedFilters.category) {
             return false
           }
           if (item.price < selectedFilters.priceRange[0] || item.price > selectedFilters.priceRange[1]) {
             return false
           }
           if (selectedFilters.inStock && !item.inStock) {
             return false
           }
           return true
         })
         .sort((a, b) => a.name.localeCompare(b.name))
     })

     // Watch for search query changes with debouncing
     const debouncedSearch = ref('')
     let searchTimeout: number

     watch(searchQuery, (newQuery) => {
       clearTimeout(searchTimeout)
       searchTimeout = setTimeout(() => {
         debouncedSearch.value = newQuery
       }, 300)
     })

     // Watch for debounced search changes
     watch(debouncedSearch, async (query) => {
       if (query.length > 2) {
         searchResults.value = await api.search(query)
       } else {
         searchResults.value = []
       }
     })

     // WatchEffect for logging (runs immediately and on dependencies change)
     watchEffect(() => {
       console.log(`Search: "${searchQuery.value}" returned ${filteredResults.value.length} results`)
     })
     ```

5. **Custom Composables Design**
   - Create reusable logic with custom composables
   - Follow naming conventions (use prefix)
   - Implement proper cleanup and lifecycle management
   - Example composable patterns:
     ```ts
     // useFetch composable for API requests
     import { ref, type Ref } from 'vue'

     interface UseFetchOptions {
       immediate?: boolean
       onSuccess?: (data: any) => void
       onError?: (error: Error) => void
     }

     function useFetch<T>(url: string, options: UseFetchOptions = {}) {
       const data = ref<T | null>(null)
       const loading = ref(false)
       const error = ref<Error | null>(null)

       const execute = async () => {
         loading.value = true
         error.value = null

         try {
           const response = await fetch(url)
           if (!response.ok) {
             throw new Error(`HTTP error! status: ${response.status}`)
           }
           const result = await response.json()
           data.value = result
           options.onSuccess?.(result)
         } catch (err) {
           const errorObj = err instanceof Error ? err : new Error('Unknown error')
           error.value = errorObj
           options.onError?.(errorObj)
         } finally {
           loading.value = false
         }
       }

       const refresh = () => execute()

       if (options.immediate !== false) {
         execute()
       }

       return {
         data: data as Ref<T | null>,
         loading,
         error,
         execute,
         refresh
       }
     }

     // useLocalStorage composable
     function useLocalStorage<T>(key: string, defaultValue: T) {
       const storedValue = localStorage.getItem(key)
       const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue

       const value = ref<T>(initialValue)

       watch(value, (newValue) => {
         localStorage.setItem(key, JSON.stringify(newValue))
       }, { deep: true })

       return value
     }
     ```

6. **Performance Optimization Techniques**
   - Use shallow reactive when deep reactivity isn't needed
   - Implement proper component lazy loading
   - Optimize large list rendering with virtual scrolling
   - Example performance patterns:
     ```ts
     import { shallowRef, shallowReactive, defineAsyncComponent } from 'vue'

     // Use shallow for large objects that don't need deep reactivity
     const largeDataset = shallowRef([])
     const chartConfig = shallowReactive({
       type: 'line',
       options: { /* large config object */ }
     })

     // Async component loading
     const AsyncChart = defineAsyncComponent({
       loader: () => import('./components/Chart.vue'),
       loadingComponent: () => import('./components/LoadingSpinner.vue'),
       errorComponent: () => import('./components/ErrorMessage.vue'),
       delay: 200,
       timeout: 3000
     })

     // Virtual scrolling for large lists
     import { computed } from 'vue'

     function useVirtualList<T>(items: Ref<T[]>, itemHeight: number, containerHeight: number) {
       const scrollTop = ref(0)

       const visibleStart = computed(() => Math.floor(scrollTop.value / itemHeight))
       const visibleEnd = computed(() => {
         const end = visibleStart.value + Math.ceil(containerHeight / itemHeight)
         return Math.min(end, items.value.length)
       })

       const visibleItems = computed(() =>
         items.value.slice(visibleStart.value, visibleEnd.value)
       )

       const offsetY = computed(() => visibleStart.value * itemHeight)
       const totalHeight = computed(() => items.value.length * itemHeight)

       return {
         visibleItems,
         offsetY,
         totalHeight,
         scrollTop
       }
     }
     ```

---

## Component Architecture

7. **Props and Emits Best Practices**
   - Define clear prop interfaces with proper validation
   - Use TypeScript for prop type safety
   - Implement proper event emission patterns
   - Example prop and emit patterns:
     ```vue
     <script setup lang="ts">
     import { computed } from 'vue'

     // Props with validation and defaults
     interface Props {
       title: string
       items: Array<{ id: string; name: string; value: any }>
       maxItems?: number
       sortable?: boolean
       multiSelect?: boolean
     }

     const props = withDefaults(defineProps<Props>(), {
       maxItems: 100,
       sortable: true,
       multiSelect: false
     })

     // Typed events
     interface Emits {
       'item-select': [item: Props['items'][0]]
       'items-change': [items: Props['items']]
       'sort': [field: string, direction: 'asc' | 'desc']
     }

     const emit = defineEmits<Emits>()

     // Computed props
     const displayItems = computed(() =>
       props.items.slice(0, props.maxItems)
     )

     // Event handlers
     const handleItemClick = (item: Props['items'][0]) => {
       emit('item-select', item)
     }

     const handleSort = (field: string) => {
       const direction = sortDirection.value === 'asc' ? 'desc' : 'asc'
       emit('sort', field, direction)
     }
     </script>
     ```

8. **Provide/Inject for Dependency Injection**
   - Use provide/inject for component tree communication
   - Create typed injection keys for type safety
   - Implement proper fallback handling
   - Example dependency injection:
     ```ts
     // types/injection-keys.ts
     import type { InjectionKey, Ref } from 'vue'

     export interface UserService {
       currentUser: Ref<User | null>
       login: (credentials: LoginCredentials) => Promise<void>
       logout: () => void
       updateProfile: (updates: Partial<User>) => Promise<void>
     }

     export const userServiceKey: InjectionKey<UserService> = Symbol('userService')

     // App.vue or main provider component
     <script setup lang="ts">
     import { provide } from 'vue'
     import { userServiceKey } from '@/types/injection-keys'
     import { useUserService } from '@/composables/useUserService'

     const userService = useUserService()
     provide(userServiceKey, userService)
     </script>

     // Child component consuming the service
     <script setup lang="ts">
     import { inject } from 'vue'
     import { userServiceKey } from '@/types/injection-keys'

     const userService = inject(userServiceKey)
     if (!userService) {
       throw new Error('UserService not provided')
     }

     // Use the injected service
     const { currentUser, login, logout } = userService
     </script>
     ```

---

## State Management and Routing

9. **Pinia State Management**
   - Use Pinia for global state management
   - Implement proper store composition patterns
   - Handle async operations in stores
   - Example Pinia store:
     ```ts
     // stores/user.ts
     import { defineStore } from 'pinia'
     import { ref, computed } from 'vue'
     import type { User, LoginCredentials } from '@/types'

     export const useUserStore = defineStore('user', () => {
       // State
       const currentUser = ref<User | null>(null)
       const loading = ref(false)
       const error = ref<string | null>(null)

       // Getters
       const isAuthenticated = computed(() => !!currentUser.value)
       const isAdmin = computed(() => currentUser.value?.role === 'admin')
       const displayName = computed(() =>
         currentUser.value ? `${currentUser.value.firstName} ${currentUser.value.lastName}` : 'Guest'
       )

       // Actions
       const login = async (credentials: LoginCredentials) => {
         loading.value = true
         error.value = null

         try {
           const response = await authApi.login(credentials)
           currentUser.value = response.user
           localStorage.setItem('token', response.token)
         } catch (err) {
           error.value = err instanceof Error ? err.message : 'Login failed'
           throw err
         } finally {
           loading.value = false
         }
       }

       const logout = () => {
         currentUser.value = null
         localStorage.removeItem('token')
       }

       const updateProfile = async (updates: Partial<User>) => {
         if (!currentUser.value) return

         try {
           const updated = await userApi.updateProfile(currentUser.value.id, updates)
           currentUser.value = { ...currentUser.value, ...updated }
         } catch (err) {
           error.value = err instanceof Error ? err.message : 'Update failed'
           throw err
         }
       }

       const initialize = async () => {
         const token = localStorage.getItem('token')
         if (token) {
           try {
             currentUser.value = await authApi.getCurrentUser()
           } catch {
             logout()
           }
         }
       }

       return {
         // State
         currentUser,
         loading,
         error,
         // Getters
         isAuthenticated,
         isAdmin,
         displayName,
         // Actions
         login,
         logout,
         updateProfile,
         initialize
       }
     })
     ```

10. **Vue Router 4 Integration**
    - Use the composition API with Vue Router
    - Implement proper route guards and navigation
    - Handle route parameters and query strings
    - Example router integration:
      ```ts
      // composables/useRouter.ts
      import { useRouter, useRoute, onBeforeRouteLeave } from 'vue-router'
      import { ref, watch } from 'vue'

      export function useNavigationGuard() {
        const router = useRouter()
        const route = useRoute()
        const hasUnsavedChanges = ref(false)

        // Navigation guard
        onBeforeRouteLeave((to, from) => {
          if (hasUnsavedChanges.value) {
            const answer = window.confirm(
              'You have unsaved changes. Are you sure you want to leave?'
            )
            if (!answer) return false
          }
        })

        // Programmatic navigation
        const navigateTo = (name: string, params?: Record<string, any>) => {
          router.push({ name, params })
        }

        // Query parameter handling
        const updateQuery = (updates: Record<string, any>) => {
          router.replace({
            query: { ...route.query, ...updates }
          })
        }

        return {
          route,
          router,
          hasUnsavedChanges,
          navigateTo,
          updateQuery
        }
      }

      // Component usage
      <script setup lang="ts">
      import { useNavigationGuard } from '@/composables/useRouter'

      const { route, hasUnsavedChanges, updateQuery } = useNavigationGuard()

      // Watch for route parameter changes
      watch(() => route.params.id, (newId) => {
        if (newId) {
          fetchUser(newId)
        }
      }, { immediate: true })

      // Update URL query when filters change
      watch(filters, (newFilters) => {
        updateQuery(newFilters)
      })
      </script>
      ```

---

## Testing and Development

11. **Component Testing Strategies**
    - Test composables independently from components
    - Use Vue Test Utils for component testing
    - Mock external dependencies properly
    - Example testing patterns:
      ```ts
      // tests/composables/useUser.test.ts
      import { describe, it, expect, vi } from 'vitest'
      import { useUser } from '@/composables/useUser'
      import * as api from '@/services/api'

      vi.mock('@/services/api')

      describe('useUser', () => {
        it('should fetch user data', async () => {
          const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' }
          vi.mocked(api.getUser).mockResolvedValue(mockUser)

          const { user, loading, fetchUser } = useUser()

          expect(loading.value).toBe(false)
          expect(user.value).toBe(null)

          await fetchUser(1)

          expect(loading.value).toBe(false)
          expect(user.value).toEqual(mockUser)
        })

        it('should handle fetch errors', async () => {
          const errorMessage = 'User not found'
          vi.mocked(api.getUser).mockRejectedValue(new Error(errorMessage))

          const { error, fetchUser } = useUser()

          await fetchUser(999)

          expect(error.value).toBe(errorMessage)
        })
      })

      // tests/components/UserProfile.test.ts
      import { mount } from '@vue/test-utils'
      import { describe, it, expect } from 'vitest'
      import UserProfile from '@/components/UserProfile.vue'

      describe('UserProfile', () => {
        it('renders user information correctly', () => {
          const user = {
            id: 1,
            name: 'John Doe',
            email: 'john@example.com'
          }

          const wrapper = mount(UserProfile, {
            props: { user }
          })

          expect(wrapper.text()).toContain('John Doe')
          expect(wrapper.text()).toContain('john@example.com')
        })

        it('emits update event when form is submitted', async () => {
          const wrapper = mount(UserProfile, {
            props: { user: { id: 1, name: 'John', email: 'john@example.com' } }
          })

          await wrapper.find('form').trigger('submit')

          expect(wrapper.emitted('update')).toBeTruthy()
        })
      })
      ```

---

## Summary Checklist

- [ ] Use script setup syntax for cleaner component code
- [ ] Implement proper reactivity with ref() and reactive()
- [ ] Create reusable logic with custom composables
- [ ] Use TypeScript for type safety throughout
- [ ] Implement proper computed and watcher patterns
- [ ] Design clear prop and emit interfaces
- [ ] Use provide/inject for dependency injection
- [ ] Integrate Pinia for global state management
- [ ] Handle routing with Vue Router 4 composition API
- [ ] Optimize performance with shallow reactivity when appropriate
- [ ] Write comprehensive tests for composables and components
- [ ] Follow Vue 3 best practices and conventions

---

Follow these patterns to build scalable, maintainable, and performant Vue 3 applications using the Composition API effectively.
Vue 3 Composition API - Cursor IDE AI Rule