Contents

Vue Best Practices

Quick Start

Installation & Version

Current version is Vue 3.5.32.

Vue 3 requires Node.js 20+.

# npm
npm create vue@latest
cd <your-project-name>
npm install
npm run dev

# pnpm
pnpm create vue@latest
cd <your-project-name>
pnpm install
pnpm run dev

# yarn
yarn create vue@latest
cd <your-project-name>
yarn
yarn dev

Using Vite

# npm
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev

# pnpm
pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
pnpm install
pnpm run dev

CDN (No Build Step)

<!-- Using ESM build -->
<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#app')
</script>

Vue 3 vs Vue 2 Key Changes

Feature Vue 2 Vue 3
Reactivity defineProperty Proxy
Components Options API Composition API
TypeScript Via decorators Native support
Slot components slot v-slot
Global state Vuex Pinia

Composition API

Core feature of Vue 3, using <script setup> syntax:

Basic Structure

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// Reactive state
const count = ref(0)
const message = ref('Hello')

// Computed
const doubled = computed(() => count.value * 2)

// Methods
function increment() {
  count.value++
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <p>{{ message }} {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

reactive

import { reactive } from 'vue'

// Object reactivity
const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    age: 30
  }
})

// Modify
state.count++
state.user.name = 'Bob'

Destructuring and Reactivity

import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  message: 'Hello'
})

// Destructure while maintaining reactivity
const { count, message } = toRefs(state)

TypeScript Integration

Defining Props

import { defineProps, PropType } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

const props = defineProps<{
  title: string
  users: User[]
  onSave: (user: User) => void
}>()

Defining Emits

const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

// Usage
emit('update', 'new value')
emit('delete', 1)

Template Refs

import { ref, onMounted } from 'vue'

const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputRef.value?.focus()
})

Component Structure

Single File Component Structure

<script setup lang="ts">
// 1. Imports
import { ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'

// 2. Props and Emits
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ (e: 'submit'): void }>()

// 3. Reactive state
const loading = ref(false)

// 4. Computed
const isValid = computed(() => props.title.length > 0)

// 5. Methods
async function handleSubmit() {
  loading.value = true
  try {
    emit('submit')
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <!-- Template -->
</template>

<style scoped>
/* Component styles */
</style>

Pinia State Management

Installation

npm install pinia

Creating a Store

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)

  // Computed
  const userCount = computed(() => users.value.length)
  const isLoggedIn = computed(() => currentUser.value !== null)

  // Actions
  async function fetchUsers() {
    const response = await api.get('/users')
    users.value = response.data
  }

  async function login(credentials: Credentials) {
    const response = await api.post('/login', credentials)
    currentUser.value = response.data
  }

  function logout() {
    currentUser.value = null
  }

  return {
    users,
    currentUser,
    userCount,
    isLoggedIn,
    fetchUsers,
    login,
    logout
  }
})

Usage in Components

<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// Usage
console.log(userStore.userCount)
userStore.login({ email: '[email protected]', password: '123' })
</script>

Vue Router

Configuration

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    props: true
  },
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guard
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return '/login'
  }
})

export default router

Usage in Components

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// Get params
const userId = route.params.id

// Navigate
router.push('/dashboard')
</script>

Composables

Encapsulate reusable logic:

Creating

// composables/useFetch.ts
import { ref, onMounted } from 'vue'

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

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, loading, error, refetch: fetchData }
}

Usage

<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'

interface User {
  id: number
  name: string
}

const { data: users, loading, error, refetch } = useFetch<User[]>('/api/users')
</script>

Performance Optimization

Lazy Loading Components

<!-- Static import -->
<script setup>
import HeavyComponent from './HeavyComponent.vue'
</script>

<!-- Dynamic import (lazy) -->
<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

v-memo

<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
    <!-- Only updates when selectedId changes -->
  </div>
</template>

KeepAlive

<template>
  <keep-alive include="UserList,ProductList">
    <router-view />
  </keep-alive>
</template>

Style Guide

Scoped CSS

<style scoped>
.button {
  padding: 8px 16px;
  border-radius: 4px;
}

/* Deep selector */
:deep(.el-input) {
  border: none;
}
</style>

CSS Variables

<style scoped>
:root {
  --primary-color: #42b983;
  --button-padding: 8px 16px;
}

.button {
  background: var(--primary-color);
  padding: var(--button-padding);
}
</style>

Testing

Vitest

npm install -D vitest @vue/test-utils jsdom

Component Testing

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

describe('Button', () => {
  it('renders with correct text', () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})