mk:vue
Vue 3 Composition API patterns, Pinia, reactivity, component design, forms, and performance. Auto-activates on .vue files.
Use
npx chub search vuefor relevant documentation packages within the Context Hub.
What This Skill Does
Provides Vue 3 best practices and patterns for building components, composables, Pinia stores, and forms. Enforces Composition API with <script setup lang="ts"> and TypeScript interfaces throughout. Prevents common reactivity pitfalls and deprecated Options API usage.
When to Use
Auto-activate on: .vue files, Vue 3 Composition API, Pinia stores, <script setup>, Vue Router, composables.
Explicit: /mk:vue [concern]
Do NOT invoke for: TypeScript fundamentals (use mk:typescript), visual design (use mk:frontend-design), testing (use mk:testing).
Core Capabilities
- Component design —
<script setup lang="ts">, TypeScript props/emits/slots, PascalCase naming - Composables — reusable reactive logic with
use*prefix, kebab-case files - Pinia stores — setup syntax with
storeToRefs()for reactive destructuring - Reactivity —
ref()for primitives,reactive()for complex objects,computed()for derived state - Performance —
v-once,v-memo,defineAsyncComponent, code splitting - Forms — VeeValidate + Zod integration (optional, when project supports)
Arguments
| Argument | Type | Description |
|---|---|---|
concern | string | Optional. Focus area: component, composable, store, form, performance |
Workflow
Operates in Phase 3 (Build GREEN). Output supports the developer agent.
Process
- Detect concern — component? composable? store? form? performance?
- Load relevant reference —
references/vue-patterns.md - Apply patterns — implement using Vue 3 best practices from references
- Verify — component renders, types pass, no console warnings
Core Rules (always apply)
- ALWAYS use
<script setup lang="ts">— never Options API - ALWAYS use
definePropswith TypeScript interfaces — never runtime validation - ALWAYS use
storeToRefs()when destructuring Pinia store state - NEVER use
v-htmlwith user content (security-rules.md — XSS vector) - PREFER composables (
use*) over mixins - PREFER
ref()for primitives,reactive()only for complex objects - NAME components PascalCase, files kebab-case (
agent-conduct.mdB1)
Anti-Patterns
| Don't | Do Instead |
|---|---|
Options API (data(), methods:) | Composition API <script setup> |
this.$store / Vuex | Pinia with setup store syntax |
| Direct store state destructuring | storeToRefs(useMyStore()) |
v-html with dynamic content | v-text or sanitized rendering |
reactive() for primitives | ref() for primitives |
| Watchers when computed works | computed() for derived state |
| Global event bus | provide/inject or Pinia |
defineComponent() wrapper | <script setup> directly |
Patterns
Component Template
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
type Slots = {
default: [];
};
type Props = {
title: string;
count?: number;
};
type Emits = {
update: [value: number];
};
const slots = defineSlots<Slots>();
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
const emit = defineEmits<Emits>();
const localCount = ref(props.count);
const doubled = computed(() => localCount.value * 2);
const increment = (): void => {
localCount.value++;
emit("update", localCount.value);
};
onMounted(() => {
// setup logic
});
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ localCount }} (doubled: {{ doubled }})</p>
<button @click="increment">+1</button>
</div>
</template>Composables
// composables/useCounter.ts
import { ref, computed } from "vue";
const useCounter = () => {
const count = ref(0);
const doubled = computed(() => count.value * 2);
const increment = (): void => { count.value++; };
const decrement = (): void => { count.value--; };
const reset = (): void => { count.value = 0; };
return { count, doubled, increment, decrement, reset };
};
export default useCounter;Naming: always use prefix, kebab-case file: use-counter.ts
Pinia Store (Setup Syntax)
// stores/use-auth-store.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useAuthStore = defineStore("auth", () => {
const user = ref<User | null>(null);
const isLoggedIn = computed(() => user.value !== null);
const login = async (credentials: LoginDto): Promise<void> => {
user.value = await authApi.login(credentials);
};
const logout = (): void => {
user.value = null;
};
return { user, isLoggedIn, login, logout };
});Usage in component — always use storeToRefs:
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useAuthStore } from "@/stores/use-auth-store";
const authStore = useAuthStore();
const { user, isLoggedIn } = storeToRefs(authStore);
// Methods destructured directly (not reactive)
const { login, logout } = authStore;
</script>Reactivity Rules
| Rule | Why |
|---|---|
ref() for primitives | .value access is explicit, avoids confusion |
reactive() for objects (if needed) | No .value, but can't reassign whole object |
computed() for derived state | Caches automatically, updates when deps change |
watch() for side effects | Async ops, API calls, logging |
watchEffect() for auto-tracking | Auto-detects dependencies |
toRef() / toRefs() for prop destructuring | Maintains reactivity when destructuring |
Performance Patterns
<!-- v-once for static content -->
<span v-once>{{ expensiveComputation }}</span>
<!-- v-memo for conditional skip -->
<div v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
<!-- Async components for code splitting -->
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
const HeavyChart = defineAsyncComponent(() => import("./HeavyChart.vue"));
</script>Form Handling (VeeValidate + Zod)
If project supports vee-validate and zod:
<script setup lang="ts">
import { useForm } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
const schema = toTypedSchema(
z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Min 8 characters"),
}),
);
const { handleSubmit, errors } = useForm({ validationSchema: schema });
const onSubmit = handleSubmit(async (values) => {
await authStore.login(values);
});
</script>Output Format
## Vue: {concern}
**Files:** {list of .vue files modified}
**Pattern:** {composition API | pinia store | composable | component}
### Implementation
{code changes with Vue 3 patterns applied}
### Verification
{component renders, no console warnings, types pass}Failure Handling
| Failure | Recovery |
|---|---|
| Vue 2 syntax detected (Options API) | Migrate to Composition API <script setup> |
| Pinia not installed | npm install pinia |
| Type errors in template | Fix with definePropsT() typing |
Gotchas
- Destructuring
reactive()loses reactivity —const { count } = reactive({ count: 0 })makescounta plain number; always usetoRefs()or keep the reactive object intact, or switch toref(). storeToRefs()not called when destructuring Pinia store —const { user } = useAuthStore()gives a non-reactive snapshot;const { user } = storeToRefs(useAuthStore())is required for reactive bindings.<script setup>withdefineExpose()is required for parentrefaccess — callingchildRef.value.method()from a parent always returnsundefinedunless the child explicitlydefineExpose({ method }); omitting this is silent until runtime.:deep()selector broken when component uses scoped styles + slot content — slot content comes from the parent scope, so:deep(.child-class)in a scoped style block has no effect on slotted content; use:slotted(.child-class)instead.watchEffectcleanup race on fast re-renders — without calling theonCleanupcallback to cancel async operations, a fast prop change triggers a second effect before the first resolves, causing stale state writes; always register cleanup viawatchEffect((onCleanup) => { onCleanup(() => controller.abort()) }).- Pinia store state not hydrated in SSR (Nuxt/Vite SSR) — calling
useMyStore()outside a component setup context (e.g., in a top-level module) creates a store instance disconnected from the SSR app; always call stores insidesetup()or pass thepiniainstance explicitly viauseMyStore(pinia).
References
| Reference | When to load | Content |
|---|---|---|
references/vue-patterns.md | Component/composable work | Composition API, reactivity, component design, performance |
Common Use Cases
- Creating new Vue 3 components with proper TypeScript typing
- Migrating from Options API to Composition API
- Setting up Pinia stores with setup syntax
- Building reusable composables
- Optimizing rendering with
v-onceandv-memo - Implementing form validation with VeeValidate + Zod
Example Prompt
/mk:vue store I'm setting up auth state management for my Vue 3 app. Generate a Pinia store with proper TypeScript typing, setup syntax, and show me how to use storeToRefs in my components so I don't lose reactivity.
Pro Tips
- Use
definePropsT()with interfaces for type safety — never runtime validation - Destructure methods directly from the store, but always use
storeToRefs()for state - Use
computed()instead ofwatch()for derived state whenever possible - Always test components after refactoring — ensure no console warnings appear
- For SSR projects, always call Pinia stores inside
setup()context