Understanding Vue's API is the foundation. Using it well in production requires knowing the patterns that scale and the mistakes that cost you later.
TypeScript in Vue 3
Vue 3 has first-class TypeScript support. Enable it in <script setup>:
<script setup lang="ts">
import { ref } from 'vue'
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
const users = ref<User[]>([])
</script>Type your props and emits:
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
close: []
}>()
</script>Performance Patterns
v-once for static content that never changes:
<template>
<h1 v-once>{{ staticTitle }}</h1>
</template>v-memo for expensive list items:
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- Only re-renders when item.selected changes -->
<ExpensiveComponent :item="item" />
</div>
</template>Lazy-load routes to reduce initial bundle size:
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue') // lazy loaded
}
]Error Handling
Global error handling for unhandled errors:
// main.ts
app.config.errorHandler = (error, instance, info) => {
console.error('Global error:', error)
// Send to error tracking service (Sentry, etc.)
}Local error handling with onErrorCaptured:
<script setup>
import { onErrorCaptured } from 'vue'
onErrorCaptured((error) => {
console.error('Child error captured:', error)
return false // prevent propagation
})
</script>Async Components
For components that are large or need async data before rendering:
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: Spinner,
errorComponent: ErrorMessage,
delay: 200, // show loading after 200ms
timeout: 3000
})Testing Vue Components
Write tests with Vitest and Vue Testing Library:
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import Counter from './Counter.vue'
test('increments count on click', async () => {
render(Counter)
const button = screen.getByRole('button', { name: /count/i })
await userEvent.click(button)
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})Project Structure
A scalable Vue project structure:
src/
components/ # Reusable UI components
composables/ # Reusable logic (useCounter, useFetch, etc.)
stores/ # Pinia stores
views/ # Page-level components (one per route)
router/ # Vue Router configuration
types/ # TypeScript interfaces and types
utils/ # Pure utility functionsKeep views thin — they compose components and connect stores. Keep business logic in composables and stores.