Skip to content

mk:vue

Vue 3 Composition API patterns, Pinia, reactivity, component design, forms, and performance. Auto-activates on .vue files.

Use npx chub search vue for 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
  • Reactivityref() for primitives, reactive() for complex objects, computed() for derived state
  • Performancev-once, v-memo, defineAsyncComponent, code splitting
  • Forms — VeeValidate + Zod integration (optional, when project supports)

Arguments

ArgumentTypeDescription
concernstringOptional. Focus area: component, composable, store, form, performance

Workflow

Operates in Phase 3 (Build GREEN). Output supports the developer agent.

Process

  1. Detect concern — component? composable? store? form? performance?
  2. Load relevant referencereferences/vue-patterns.md
  3. Apply patterns — implement using Vue 3 best practices from references
  4. Verify — component renders, types pass, no console warnings

Core Rules (always apply)

  • ALWAYS use <script setup lang="ts"> — never Options API
  • ALWAYS use defineProps with TypeScript interfaces — never runtime validation
  • ALWAYS use storeToRefs() when destructuring Pinia store state
  • NEVER use v-html with 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.md B1)

Anti-Patterns

Don'tDo Instead
Options API (data(), methods:)Composition API <script setup>
this.$store / VuexPinia with setup store syntax
Direct store state destructuringstoreToRefs(useMyStore())
v-html with dynamic contentv-text or sanitized rendering
reactive() for primitivesref() for primitives
Watchers when computed workscomputed() for derived state
Global event busprovide/inject or Pinia
defineComponent() wrapper<script setup> directly

Patterns

Component Template

vue
<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

typescript
// 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)

typescript
// 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:

vue
<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

RuleWhy
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 stateCaches automatically, updates when deps change
watch() for side effectsAsync ops, API calls, logging
watchEffect() for auto-trackingAuto-detects dependencies
toRef() / toRefs() for prop destructuringMaintains reactivity when destructuring

Performance Patterns

vue
<!-- 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:

vue
<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

FailureRecovery
Vue 2 syntax detected (Options API)Migrate to Composition API <script setup>
Pinia not installednpm install pinia
Type errors in templateFix with definePropsT() typing

Gotchas

  • Destructuring reactive() loses reactivityconst { count } = reactive({ count: 0 }) makes count a plain number; always use toRefs() or keep the reactive object intact, or switch to ref().
  • storeToRefs() not called when destructuring Pinia storeconst { user } = useAuthStore() gives a non-reactive snapshot; const { user } = storeToRefs(useAuthStore()) is required for reactive bindings.
  • <script setup> with defineExpose() is required for parent ref access — calling childRef.value.method() from a parent always returns undefined unless the child explicitly defineExpose({ 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.
  • watchEffect cleanup race on fast re-renders — without calling the onCleanup callback to cancel async operations, a fast prop change triggers a second effect before the first resolves, causing stale state writes; always register cleanup via watchEffect((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 inside setup() or pass the pinia instance explicitly via useMyStore(pinia).

References

ReferenceWhen to loadContent
references/vue-patterns.mdComponent/composable workComposition 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-once and v-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 of watch() 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

Released under the MIT License.