Claude
Skills
Sign in
Back

vue-development

Included with Lifetime
$97 forever

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.

Web Dev

What this skill does


# 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
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";

// Props with TypeScript
interface Props {
  title: string;
  count?: number;
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
});

// Emits with TypeScript
const emit = defineEmits<{
  update: [value: number];
  submit: [];
}>();

// Reactive state
const localCount = ref(props.count);
const items = ref<string[]>([]);

// Computed
const doubleCount = computed(() => localCount.value * 2);

// Methods
function increment() {
  localCount.value++;
  emit("update", localCount.value);
}

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

<template>
  <div class="component">
    <h1>{{ title }}</h1>
    <p>Count: {{ localCount }} (Double: {{ doubleCount }})</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<style scoped>
.component {
  padding: 1rem;
}
</style>
```

### 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<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(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<User[]>("/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<T>(key: string, defaultValue: T): Ref<T> {
  // Read initial value
  const stored = localStorage.getItem(key);
  const value = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;

  // 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<User | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(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
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user";

const userStore = useUserStore();

// Use storeToRefs for destructuring state/getters
const { user, isLoggedIn, loading } = storeToRefs(userStore);

// Actions can be destructured directly
const { login, logout } = userStore;

async function handleLogin() {
  try {
    await login(email.value, password.value);
    router.push("/dashboard");
  } catch {
    // Error handled in store
  }
}
</script>
```

### 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<User | null>;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthKey: InjectionKey<AuthContext> = Symbol("auth");

// Provider component
export function provideAuth() {
  const user = ref<User | null>(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
Files: 1
Size: 14.6 KB
Complexity: 19/100
Category: Web Dev

Related in Web Dev