--- name: vue-development description: Vue.js 3 development with Composition API, TypeScript, Pinia state management, and Nuxt 3 full-stack. Use when building Vue applications, implementing reactive patterns, creating composables, or working with the Vue ecosystem. --- # Vue.js 3 Development Comprehensive guide for building modern Vue.js applications with the Composition API. ## Stack Overview | Tool | Purpose | Version | | ---------- | -------------------- | ------- | | Vue 3 | Core framework | 3.4+ | | TypeScript | Type safety | 5.0+ | | Vite | Build tool | 5.0+ | | Pinia | State management | 2.1+ | | Vue Router | Routing | 4.2+ | | Nuxt 3 | Full-stack framework | 3.10+ | | Vitest | Testing | 1.0+ | --- ## Composition API with Script Setup ### Basic Component Structure ```vue {{ title }} Count: {{ localCount }} (Double: {{ doubleCount }}) Increment ``` ### Reactivity Patterns ```typescript import { ref, reactive, computed, watch, watchEffect } from "vue"; // Primitives: use ref() const count = ref(0); const name = ref(""); count.value++; // Access via .value // Objects/Arrays: use reactive() const state = reactive({ items: [] as Item[], loading: false, error: null as Error | null, }); state.items.push(item); // Direct access, no .value // Computed values const total = computed(() => state.items.reduce((sum, i) => sum + i.price, 0)); // Watch specific values watch(count, (newVal, oldVal) => { console.log(`Count changed: ${oldVal} -> ${newVal}`); }); // Watch with options watch( () => state.items, (newItems) => saveToStorage(newItems), { deep: true, immediate: true }, ); // Automatic dependency tracking watchEffect(() => { console.log(`Count is now: ${count.value}`); }); ``` --- ## Composables (Reusable Logic) ### Naming & Structure ``` src/ └── composables/ ├── useFetch.ts ├── useLocalStorage.ts ├── useDebounce.ts └── useAuth.ts ``` **Convention:** Always prefix with `use` (e.g., `useFetch`, `useAuth`) ### Creating Composables ```typescript // composables/useFetch.ts import { ref, toValue, watchEffect, type MaybeRefOrGetter } from "vue"; export function useFetch(url: MaybeRefOrGetter) { const data = ref(null); const error = ref(null); const loading = ref(false); async function fetchData() { loading.value = true; error.value = null; try { const response = await fetch(toValue(url)); if (!response.ok) throw new Error(`HTTP ${response.status}`); data.value = await response.json(); } catch (e) { error.value = e instanceof Error ? e : new Error(String(e)); } finally { loading.value = false; } } // Auto-refetch when URL changes watchEffect(() => { fetchData(); }); return { data, error, loading, refetch: fetchData }; } // Usage in component const { data, error, loading } = useFetch("/api/users"); // With reactive URL const userId = ref(1); const { data: user } = useFetch(() => `/api/users/${userId.value}`); ``` ### Composable Best Practices ```typescript // composables/useLocalStorage.ts import { ref, watch, type Ref } from "vue"; export function useLocalStorage(key: string, defaultValue: T): Ref { // Read initial value const stored = localStorage.getItem(key); const value = ref(stored ? JSON.parse(stored) : defaultValue) as Ref; // Sync to storage watch( value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)); }, { deep: true }, ); return value; } // ✅ Good: Accept refs, getters, or plain values // ✅ Good: Return plain object with refs (not reactive) // ✅ Good: Clean up in onUnmounted // ✅ Good: Use toValue() for flexible inputs ``` --- ## Pinia State Management ### Store Definition (Composition Style) ```typescript // stores/user.ts import { defineStore } from "pinia"; import { ref, computed } from "vue"; export interface User { id: number; name: string; email: string; } export const useUserStore = defineStore("user", () => { // State const user = ref(null); const loading = ref(false); const error = ref(null); // Getters (computed) const isLoggedIn = computed(() => !!user.value); const displayName = computed(() => user.value?.name ?? "Guest"); // Actions async function login(email: string, password: string) { loading.value = true; error.value = null; try { const response = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password }), headers: { "Content-Type": "application/json" }, }); if (!response.ok) throw new Error("Login failed"); user.value = await response.json(); } catch (e) { error.value = e instanceof Error ? e.message : "Unknown error"; throw e; } finally { loading.value = false; } } function logout() { user.value = null; } return { // State user, loading, error, // Getters isLoggedIn, displayName, // Actions login, logout, }; }); ``` ### Using Stores in Components ```vue ``` ### Store with Persistence ```typescript // stores/settings.ts import { defineStore } from "pinia"; import { ref, watch } from "vue"; export const useSettingsStore = defineStore("settings", () => { const theme = ref<"light" | "dark">("light"); const locale = ref("en"); // Load from storage on init const stored = localStorage.getItem("settings"); if (stored) { const parsed = JSON.parse(stored); theme.value = parsed.theme ?? "light"; locale.value = parsed.locale ?? "en"; } // Persist changes watch([theme, locale], () => { localStorage.setItem( "settings", JSON.stringify({ theme: theme.value, locale: locale.value, }), ); }); function toggleTheme() { theme.value = theme.value === "light" ? "dark" : "light"; } return { theme, locale, toggleTheme }; }); ``` --- ## Component Patterns ### Provider/Inject Pattern ```typescript // context/auth.ts import { provide, inject, type InjectionKey } from "vue"; interface AuthContext { user: Ref; login: (email: string, password: string) => Promise; logout: () => void; } const AuthKey: InjectionKey = Symbol("auth"); // Provider component export function provideAuth() { const user = ref(null); async function login(email: string, password: string) { // Implementation } function logout() { user.value = null; } const context = { user, login, logout }; provide(AuthKey, context); return context; } // Consumer hook export function useAuth(): AuthContext { const context = inject(AuthKey); if (!context) { throw new Error("useAuth must be used within AuthProvider"); } return context; } ``` ### Smart/Dumb Components ```vue {{ name }} {{ email }} ``` ```vue Loading... Error: {{ error.message }} ``` --- ## Nuxt 3 (Full-Stack) ### Project Structure ``` nuxt-app/ ├── pages/ # File-based routing │ ├── index.vue # / │ ├── about.vue # /about │ └── users/ │ ├── index.vue # /users │ └── [id].vue # /users/:id ├── components/ # Auto-imported components ├── composables/ # Auto-imported composables ├── server/ # API routes (Nitro) │ ├── api/ │ │ └── users.ts # /api/users │ └── middleware/ ├── layouts/ # Page layouts ├── middleware/ # Route middleware └── nuxt.config.ts ``` ### API Routes ```typescript // server/api/users.ts export default defineEventHandler(async (event) => { const method = getMethod(event); if (method === "GET") { return await prisma.user.findMany(); } if (method === "POST") { const body = await readBody(event); return await prisma.user.create({ data: body }); } }); // server/api/users/[id].ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, "id"); return await prisma.user.findUnique({ where: { id: Number(id) } }); }); ``` ### Data Fetching ```vue ``` --- ## Testing with Vitest ### Setup ```typescript // vitest.config.ts import { defineConfig } from "vitest/config"; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], test: { globals: true, environment: "jsdom", setupFiles: ["./tests/setup.ts"], }, }); // tests/setup.ts import { vi } from "vitest"; import "@testing-library/jest-dom/vitest"; ``` ### Component Testing ```typescript // tests/components/UserCard.test.ts import { describe, it, expect } from "vitest"; import { render, screen, fireEvent } from "@testing-library/vue"; import UserCard from "@/components/UserCard.vue"; describe("UserCard", () => { it("renders user information", () => { render(UserCard, { props: { name: "John Doe", email: "john@example.com", }, }); expect(screen.getByText("John Doe")).toBeInTheDocument(); expect(screen.getByText("john@example.com")).toBeInTheDocument(); }); it("emits click event", async () => { const { emitted } = render(UserCard, { props: { name: "John", email: "john@test.com" }, }); await fireEvent.click(screen.getByRole("article")); expect(emitted().click).toHaveLength(1); }); }); ``` ### Composable Testing ```typescript // tests/composables/useFetch.test.ts import { describe, it, expect, vi, beforeEach } from "vitest"; import { useFetch } from "@/composables/useFetch"; import { flushPromises } from "@vue/test-utils"; describe("useFetch", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); it("fetches data successfully", async () => { const mockData = [{ id: 1, name: "Test" }]; vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockData), } as Response); const { data, loading, error } = useFetch("/api/test"); expect(loading.value).toBe(true); await flushPromises(); expect(loading.value).toBe(false); expect(data.value).toEqual(mockData); expect(error.value).toBeNull(); }); it("handles errors", async () => { vi.mocked(fetch).mockRejectedValueOnce(new Error("Network error")); const { data, error } = useFetch("/api/test"); await flushPromises(); expect(data.value).toBeNull(); expect(error.value?.message).toBe("Network error"); }); }); ``` --- ## Anti-Patterns to Avoid | Anti-Pattern | Problem | Solution | | ---------------------------------------- | ------------------------------- | ------------------------ | | Using Options API for complex components | Poor code organization | Use Composition API | | Mutating props directly | Breaks one-way data flow | Emit events to parent | | Using `$refs` for state | Not reactive | Use `ref()` and props | | Deeply nested watchers | Performance issues | Use `computed()` instead | | Using mixins | Name collisions, unclear source | Use composables | | Not using `storeToRefs()` | Loses reactivity on destructure | Always use with Pinia | --- ## Related Resources - [Vue.js Official Docs](https://vuejs.org/) - [Pinia Documentation](https://pinia.vuejs.org/) - [Nuxt 3 Documentation](https://nuxt.com/) - [VueUse Composables](https://vueuse.org/) - [Vue Test Utils](https://test-utils.vuejs.org/) --- ## When to Use This Skill - Building new Vue.js 3 applications - Migrating from Vue 2 Options API to Composition API - Implementing Pinia state management - Creating reusable composables - Full-stack development with Nuxt 3 - Testing Vue components with Vitest
Count: {{ localCount }} (Double: {{ doubleCount }})
{{ email }}