option
HomeHome Skill Web Development vue-patterns

vue-patterns

affaan-m/ECC affaan-m/ECC

Provides Vue.js 3 Composition API patterns, component architecture, reactivity best practices, Pinia state management, Vue Router navigation, and Nuxt SSR patterns for Vue, Nuxt, Vite, or Pinia projects.

...Expand all
0
Updated time October 2, 2026

Vue.js Patterns and Best Practices

Comprehensive guide for Vue.js 3 development using Composition API (

Presentational vs Container

  • Container components: Own data fetching, state, and side effects. Render presentational components.
  • Presentational components: Receive props, emit events. No API calls, no store access. Pure rendering.

Props Best Practices

// Type-based props with defaults
interface Props {
  label: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
  items: Item[];
}

const props = withDefaults(defineProps(), {
  variant: "primary",
  disabled: false,
});
  • Always provide type, and required/default where appropriate.
  • Boolean props: isXxx, hasXxx, canXxx.
  • Never mutate props — emit events instead.
  • For v-model binding, use defineModel() (Vue 3.4+) or modelValue + update:modelValue.

Events

const emit = defineEmits<{
  submit: [];
  "update:modelValue": [value: string];
  select: [id: string, index: number];
}>();
  • Use kebab-case in templates (@update:model-value).
  • Use camelCase in script (emit("update:modelValue", val)).

3. Composables (Reusable Logic)

Structure

// composables/useDebounce.ts
export function useDebounce(value: MaybeRef, delay: number): Ref {
  const debounced = ref(toValue(value)) as Ref;

  let timer: ReturnType;
  watch(
    () => toValue(value),
    (newVal) => {
      clearTimeout(timer);
      timer = setTimeout(() => { debounced.value = newVal; }, delay);
    }
  );

  onUnmounted(() => clearTimeout(timer));
  return readonly(debounced);
}

Rules

  • Must start with use prefix.
  • Return reactive values (ref, computed, reactive), never plain primitives.
  • Accept reactive inputs via MaybeRef / toRef() / toValue().
  • Clean up side effects in onUnmounted or watcher onCleanup.
  • No module-scope side effects.

vs Mixins

Composables replace Vue 2 mixins entirely:

  • Mixins: Opaque data flow, source-of-truth collisions, name conflicts.
  • Composables: Explicit imports, clear return values, composable and tree-shakable.

4. State Management

When to Use What

Pattern Use Case
ref() / reactive() Local component state
Props + Emits Parent-child communication
Provide / Inject Theme, config, plugin API
Pinia store Global, shared, complex state
Server state composable API data with caching (wrap fetch/TanStack Query)

Pinia Setup Store (Preferred)

// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
  const items = ref([]);
  const isLoading = ref(false);

  const totalPrice = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
  );
  const itemCount = computed(() =>
    items.value.reduce((sum, i) => sum + i.quantity, 0)
  );

  async function addItem(productId: string) {
    isLoading.value = true;
    try {
      const item = await fetchProduct(productId);
      const existing = items.value.find(i => i.id === item.id);
      if (existing) existing.quantity++;
      else items.value.push({ ...item, quantity: 1 });
    } finally {
      isLoading.value = false;
    }
  }

  return { items, isLoading, totalPrice, itemCount, addItem };
});
  • Use Setup Store syntax (not Options Store).
  • Prefer actions for business-level mutations and $patch() for grouped updates.
  • Every async action: handle loading + success + error.

5. Vue Router

Route Definitions

const routes = [
  {
    path: "/users/:id",
    name: "user-detail",
    component: () => import("@/pages/UserDetail.vue"), // lazy
    props: true, // pass params as props
    meta: { requiresAuth: true },
  },
];

Navigation Guards

router.beforeEach((to, from) => {
  const { isLoggedIn } = useAuthStore();
  if (to.meta.requiresAuth && !isLoggedIn) {
    return { name: "login", query: { redirect: to.fullPath } };
  }
});

Reactive Route Params

When a component stays mounted but route params change:

const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));

6. Template Patterns

Template Syntax


Loading...
Error: {{ error }}
{{ content }}
Toggled content
{{ item.name }}
{{ item.name }}

7. Performance

Technique When to Use
v-memo List items that rarely change
v-once Content rendered once and static forever
shallowRef() Large data structures replaced wholesale
shallowReactive() Only top-level properties are reactive
v-show over v-if Frequent visibility toggles
Cache toggled views
Lazy routes () => import(...) for non-critical routes
Suspense Async component loading with fallback

8. Testing

Stack

  • Vitest for unit and component tests
  • Vue Test Utils for mounting and interaction
  • @pinia/testing for store mocking
  • Playwright for E2E

Component Test Pattern

import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";

beforeEach(() => { setActivePinia(createPinia()); });

it("renders and emits", async () => {
  const wrapper = mount(UserCard, {
    props: { user: { id: "1", name: "Alice" } },
  });
  expect(wrapper.text()).toContain("Alice");
  await wrapper.find("button").trigger("click");
  expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});

9. Nuxt-Specific Patterns

Auto-Imports

Nuxt auto-imports ref, computed, watch, useFetch, useAsyncData, etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.

useAsyncData / useFetch

const { data: user, pending, error, refresh } = await useAsyncData(
  "user", // unique key for caching
  () => $fetch(`/api/users/${id}`),
);

const { data: posts } = await useFetch("/api/posts", {
  query: { page: 1 },
  key: "posts-page-1", // dedupes requests
});

Server Routes

// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
  const { id } = await getValidatedRouterParams(event, z.object({
    id: z.string().uuid(),
  }).parse);
  // ... fetch and return
});

Runtime Config

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // server-only
    apiSecret: "",
    // public (exposed to client)
    public: {
      apiBase: "https://api.example.com",
    },
  },
});

10. Vue 3.5+ New APIs

Reactive Props Destructure

Vue 3.5 stabilized reactive props destructure — destructured variables from defineProps() are automatically reactive:

// Vue 3.5+: destructured props are reactive (no need for toRefs)
const { count = 0, msg = "hello" } = defineProps<{
  count?: number;
  msg?: string;
}>();

// Limitation: cannot watch destructured prop directly
watch(() => count, (newVal) => { ... }); // PASS getter required

useTemplateRef()

Replace name-matched plain refs with useTemplateRef() for template references:

import { useTemplateRef } from "vue";
const inputEl = useTemplateRef("input");
// "input" matches the ref="input" attribute in template, not the variable name

Supports dynamic ref IDs: useTemplateRef(dynamicRefId).

onWatcherCleanup()

Globally importable watcher cleanup API (Vue 3.5+). It must be called synchronously inside the watcher callback:

import { watch, onWatcherCleanup } from "vue";

watch(userId, async (newId) => {
  const controller = new AbortController();
  onWatcherCleanup(() => controller.abort());
  // ... fetch with signal
});

useId()

SSR-stable unique ID generation for form elements and accessibility:

import { useId } from "vue";
const id = useId();

defer Teleport

allows teleporting to targets rendered in the same cycle:

Content

Lazy Hydration (SSR)

defineAsyncComponent() now supports hydrate strategy:

import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
  loader: () => import("./Comp.vue"),
  hydrate: hydrateOnVisible(),
});

Anti-Patterns

Anti-Pattern Why It's Wrong The Fix
Destructuring defineProps() (Vue < 3.5) Captures snapshot, loses reactivity Access via props.xxx or use toRefs()
watch() on destructured prop (Vue 3.5+) Compile-time error — destructured props can't be watched directly Use getter wrapper: watch(() => count, ...)
v-if + v-for on same element Ambiguous execution order Use computed filtered array
v-for key = index Broken state on reorder Use stable database IDs
Mutating props Violates one-way data flow Emit events or use v-model
v-html with user content XSS vulnerability Sanitize with DOMPurify
Mixins in Vue 3 Opaque, collision-prone Replace with composables
Module-scope side effects in composable Shared across instances Scope in onMounted + onUnmounted
reactive() for replaceable state Replacement breaks reactivity Use ref() instead
Watcher without cleanup Memory leaks, race conditions Use onCleanup or onWatcherCleanup() (Vue 3.5+)
Options API in new Vue 3 code Ecosystem move to Composition API Use

All Files

1 files

Install vue-patterns

Download and extract the skill files to your .claude/skills/ directory.

Download ZIP

Clone the repository and copy the skill files to your project.

git clone https://github.com/affaan-m/ECC/tree/main/skills/vue-patterns # Copy SKILL.md to your .claude/skills/ directory

Copy Copy
Quick Setup: Copy the skill folder to .claude/skills/ Claude will automatically detect and use the skill
Repository affaan-m/ECC

Related Skills

github-code-search
Updated time June 29, 2026
drizzle-orm
Updated time June 29, 2026
clickhouse-io
Updated time June 29, 2026
prisma-client-api
Updated time June 29, 2026