Install
$ agentstack add skill-affaan-m-ecc-vue-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Vue.js Patterns and Best Practices
Comprehensive guide for Vue.js 3 development using Composition API (``), covering component design, reactivity, state management, routing, testing, and SSR patterns. Nuxt-specific guidance is included where it differs from vanilla Vue.
When to Activate
Activate this skill when:
- The project uses Vue.js (any version), Nuxt, Vite + Vue, or Pinia.
- The user asks about Vue component architecture, composables, reactivity, or state management.
- Reviewing Vue Single-File Components (
.vuefiles). - Setting up Vue Router, Pinia stores, or Vite/Vitest configuration.
- Discussing Vue-specific performance, security, or SSR patterns.
1. Project Structure
Recommended Layout (Feature-First)
src/
├── api/ # API client and endpoint definitions
├── assets/ # Static assets (images, fonts, icons)
├── components/ # Shared/reusable components
│ ├── base/ # Base UI primitives (Button, Input, Modal)
│ └── features/ # Feature-specific shared components
├── composables/ # Reusable Composition API logic
├── layouts/ # Page layouts (optional)
├── pages/ # Route-level page components
├── router/ # Vue Router configuration
├── stores/ # Pinia stores
├── types/ # TypeScript type definitions
├── utils/ # Pure utility functions
└── App.vue # Root component
File Naming
| Convention | When to Use | |-----------|-------------| | PascalCase.vue | All components (enforced by vue/multi-word-component-names) | | useCamelCase.ts | Composables | | camelCase.ts | Utilities, API clients, types | | kebab-case directories | Route segments, feature folders |
2. Component Architecture
Single-File Component Order
// 1. Imports (vue → ecosystem → absolute → relative)
// 2. Props & Emits & Slots
// 3. Composables
// 4. Local state (ref/reactive)
// 5. Computed properties
// 6. Methods
// 7. Watchers
// 8. Lifecycle hooks
/* Scoped styles */
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, andrequired/defaultwhere appropriate. - Boolean props:
isXxx,hasXxx,canXxx. - Never mutate props — emit events instead.
- For v-model binding, use
defineModel()(Vue 3.4+) ormodelValue+update:modelValue.
Events
const emit = defineEmits();
- 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
useprefix. - Return reactive values (
ref,computed,reactive), never plain primitives. - Accept reactive inputs via
MaybeRef/toRef()/toValue(). - Clean up side effects in
onUnmountedor watcheronCleanup. - 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 }}
Save
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();
// 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 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 | | Plain ref for template references | No dynamic ref support, name-matching fragile | Use useTemplateRef()` (Vue 3.5+) |
Related Skills
accessibility— ARIA, semantic HTML, focus managementfrontend-patterns— Cross-framework frontend architecturetypescript— TypeScript best practices applied to Vue projectscoding-standards— General code quality standards
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: affaan-m
- Source: affaan-m/ECC
- License: MIT
- Homepage: https://ecc.tools
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.