Skip to content
cccskills
FreePublished on GitHub

vue-conventions

Vue 3 SFC structure, Composition API + <script setup>, file naming, project layout, props/emits/slots typing, defineModel, composables, lifecycle, watchers, UI library detection. Vue 2 fallback notes. Use this skill to: - Structure a new SFC with `<script setup>`, template, scoped styles. - Type props/emits/slots correctly via macros. - Pick `ref` vs `reactive` (prefer `ref`). - Build composables that compose cleanly. - Apply lifecycle hooks correctly. - Detect UI library (Vuetify/Quasar/PrimeVue/Naive UI/Element Plus/shadcn-vue) and mirror its patterns. Do NOT use this skill for: - State management lib choice (see vue-state-management). - Routing (see vue-routing). - Forms (see vue-forms). - Testing (see vue-testing).

The English summary is being prepared. Showing the original description.

Run score Not tested

See how to install

Files included (1)

  • SKILL.md12.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Vue 3 Conventions

This skill consolidates idioms for Vue 3 SPA projects with <script setup> and the Composition API. Apply alongside js-foundation:typescript-patterns (general TS strictness).

Vue 2 fallback notes are at the end — applied only when project is on "vue": "^2".

Project layout

src/
├── main.ts                          # createApp + mount
├── App.vue                          # root SFC
├── router/
│   ├── index.ts                     # createRouter + routes
│   └── guards.ts                    # navigation guards
├── views/                           # route components (page-level)
│   ├── HomeView.vue
│   └── UsersView.vue
├── components/
│   ├── ui/                          # primitives (Button, Input, Modal)
│   └── features/                    # feature-specific
│       └── users/
│           ├── UserCard.vue
│           └── UserFilter.vue
├── composables/                     # use*.ts
│   ├── useDebounce.ts
│   └── useUsers.ts
├── stores/                          # Pinia stores
│   └── users.ts
├── lib/                             # framework-agnostic utilities
│   └── http.ts
├── types/
│   └── user.ts
└── assets/
    └── styles/
        └── global.css

Mirror what exists. Don't restructure as part of feature work.

File naming

WhatConvention
SFC filePascalCase.vue (UserCard.vue)
View (route)*View.vue suffix common (UsersView.vue) — match project
ComposableuseCamelCase.ts (useDebounce.ts)
Pinia storekebab-case.ts or camelCase.ts (users.ts)
Test file*.spec.ts colocated, OR mirror in tests/

Component name in <template>: PascalCase (<UserCard />) OR kebab-case (<user-card />) — both work, Vue handles conversion.

SFC structure

<script setup lang="ts">
// 1. Imports
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import type { User } from '@/types/user';

// 2. Props/emits/slots
interface Props {
  userId: string;
  initialName?: string;
}
const props = withDefaults(defineProps<Props>(), { initialName: '' });
const emit = defineEmits<{
  save: [name: string];
  cancel: [];
}>();

// 3. Reactive state
const name = ref(props.initialName);
const error = ref<string | null>(null);

// 4. Composables
const router = useRouter();

// 5. Computed
const isValid = computed(() => name.value.length >= 2);

// 6. Methods
function onSave() {
  if (!isValid.value) {
    error.value = 'Name must be at least 2 characters';
    return;
  }
  emit('save', name.value);
}

// 7. Lifecycle
onMounted(() => {
  console.log('UserForm mounted for', props.userId);
});
</script>

<template>
  <form @submit.prevent="onSave">
    <input v-model="name" placeholder="Name" />
    <p v-if="error" role="alert">{{ error }}</p>
    <div class="actions">
      <button :disabled="!isValid">Save</button>
      <button type="button" @click="emit('cancel')">Cancel</button>
    </div>
  </form>
</template>

<style scoped>
form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.actions {
  display: flex;
  gap: 8px;
}
</style>

Section order is convention; pick per project. <script setup> is the modern preferred form.

When NOT to use <script setup>

  • Components that need name for recursive references (<script> with name: option). Workaround: separate <script> block with just name.
  • Components with complex render functions instead of templates.

These are rare; most Vue 3 components use <script setup>.

Reactivity primitives

ref (preferred)

import { ref } from 'vue';

const count = ref(0);
const user = ref({ name: '', email: '' });
const items = ref<string[]>([]);

count.value++;                         // .value in JS
user.value.name = 'Alice';
items.value.push('hi');

In <template>, refs auto-unwrap — no .value needed:

<template>
  <p>{{ count }}</p>           <!-- not {{ count.value }} -->
  <p>{{ user.name }}</p>
</template>

reactive (sometimes)

import { reactive } from 'vue';

const state = reactive({ count: 0, items: [] });
state.count++;                         // no .value
state.items.push('hi');

Footgun: destructuring loses reactivity:

const state = reactive({ count: 0 });
const { count } = state;               // count is a snapshot, not reactive
count++;                                // doesn't update state.count

Workaround: toRefs():

const { count } = toRefs(state);       // count is now Ref<number>
count.value++;                          // updates state.count

Recommendation: use ref for everything. Works for primitives AND objects, no destructure footgun. Reach for reactive only when team convention says so.

computed

const double = computed(() => count.value * 2);

// Writable computed
const fullName = computed({
  get: () => `${first.value} ${last.value}`,
  set: (val) => {
    [first.value, last.value] = val.split(' ');
  },
});

watch and watchEffect

// watch — explicit deps, gets old value
watch(count, (newVal, oldVal) => {
  console.log(`${oldVal} → ${newVal}`);
});

// Watch multiple sources
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {});

// Deep watch on object refs
watch(user, (newUser) => {}, { deep: true });

// Immediate (run once on mount)
watch(count, () => {}, { immediate: true });

// watchEffect — auto-tracks deps, no oldVal
watchEffect(() => {
  console.log(`count: ${count.value}`);  // runs whenever count changes
});

// Cleanup function (e.g., unsubscribe)
watchEffect((onCleanup) => {
  const id = setInterval(() => {}, 1000);
  onCleanup(() => clearInterval(id));
});

watchEffect runs once immediately, then on each dep change. watch runs only on dep change unless immediate: true.

Props typing

// Basic
interface Props {
  title: string;
  count?: number;
}
const props = defineProps<Props>();

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

// Default for objects/arrays — function form
interface ListProps {
  items: string[];
  config?: { showHeader?: boolean };
}
const props = withDefaults(defineProps<ListProps>(), {
  items: () => [],
  config: () => ({ showHeader: true }),
});

Mark required vs optional via ?. Don't validate at runtime when TS handles it at compile time.

Emits typing

// Tuple syntax — preferred
const emit = defineEmits<{
  change: [value: string];
  submit: [data: FormData];
  delete: [id: string, force?: boolean];
}>();

emit('change', 'new value');
emit('submit', formData);
emit('delete', 'id-123', true);

Older Vue 3 syntax (function-based) still works but tuple syntax is cleaner.

Slots typing

defineSlots<{
  default(props: { user: User }): any;
  header?(props: { count: number }): any;
}>();
<template>
  <slot name="header" :count="items.length" />
  <slot v-for="item in items" :key="item.id" :user="item" />
</template>

Parent uses scoped slots:

<UserList :items="users">
  <template #header="{ count }">
    <h2>{{ count }} users</h2>
  </template>
  <template #default="{ user }">
    <UserCard :user="user" />
  </template>
</UserList>

defineModel() (Vue 3.4+)

Two-way binding without manual props+emits:

<!-- CustomInput.vue -->
<script setup lang="ts">
const value = defineModel<string>();
const required = defineModel<boolean>('required', { default: false });
</script>

<template>
  <input v-model="value" :required="required" />
</template>

Parent:

<CustomInput v-model="form.email" v-model:required="form.emailRequired" />

For Vue 3.3 and earlier, use the manual props+emits pattern.

Composables

// src/composables/useDebounce.ts
import { ref, watch, onBeforeUnmount } from 'vue';
import type { Ref } from 'vue';

export function useDebounce<T>(value: Ref<T>, delay = 300): Ref<T> {
  const debounced = ref(value.value) as Ref<T>;
  let timer: ReturnType<typeof setTimeout> | undefined;

  watch(value, (newVal) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => { debounced.value = newVal; }, delay);
  });

  onBeforeUnmount(() => { if (timer) clearTimeout(timer); });

  return debounced;
}

Conventions:

  • Name useFooBar().
  • Take refs as input, return refs as output.
  • Cleanup side effects in onBeforeUnmount.
  • Pure composables (no DOM access) → easy to test.

VueUse (@vueuse/core) provides 200+ pre-built composables — check before writing your own.

Lifecycle hooks

import { onMounted, onBeforeMount, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onActivated, onDeactivated } from 'vue';

onMounted(() => { /* DOM ready */ });
onBeforeUnmount(() => { /* cleanup */ });
onErrorCaptured((err, instance, info) => { /* error boundary */ return false; });

onActivated / onDeactivated fire when wrapped in <KeepAlive>.

In <script setup>, hooks register implicitly via call site. No need to return them.

UI library detection

The agent should detect what's installed and mirror its patterns. Common Vue UI libraries:

LibraryMarker (in deps)Style
VuetifyvuetifyMaterial Design components, opinionated grid
Quasarquasar, @quasar/cliCross-platform (web/SSR/SPA/PWA/desktop), large
PrimeVueprimevueComprehensive, themeable
Naive UInaive-uiModern TS-first, no heavy theme system
Element Pluselement-plusPopular in Asia, mature
shadcn-vue (radix-vue)radix-vueHeadless primitives, copy-paste components
HeadlessUI Vue@headlessui/vueHeadless from Tailwind Labs

Rule: never introduce a new UI lib without BA approval. Detect what's installed; mirror its patterns. If multiple UI libs present (gradual migration), match the area you're touching.

Anti-patterns

  • ❌ <script> without setup for new Vue 3 components — boilerplate without benefit.
  • ❌ Mixing Options API and Composition API in same component.
  • ❌ Destructuring reactive() objects without toRefs().
  • ❌ Mutating props directly (props.user.name = 'X') — use defineModel or emit.
  • ❌ Logic in template expressions ({{ items.filter(...).map(...).join(', ') }}) — extract to computed.
  • ❌ v-html without sanitization.
  • ❌ Forgetting :key on v-for lists, or using array index as key for reorderable lists.
  • ❌ process.env.X for build-time secrets — use import.meta.env.VITE_* (PUBLIC by definition).
  • ❌ Long composables that "do everything" — split by concern.

Vue 2 fallback (legacy)

When "vue": "^2" detected, apply minimum-effort patterns:

<script>
export default {
  name: 'UserForm',
  props: {
    userId: { type: String, required: true },
    initialName: { type: String, default: '' },
  },
  data() {
    return {
      name: this.initialName,
    };
  },
  computed: {
    isValid() {
      return this.name.length >= 2;
    },
  },
  methods: {
    onSave() {
      if (!this.isValid) return;
      this.$emit('save', this.name);
    },
  },
  mounted() {
    console.log('mounted');
  },
};
</script>

<template>
  <form @submit.prevent="onSave">
    <input v-model="name" placeholder="Name" />
    <button :disabled="!isValid">Save</button>
  </form>
</template>

Differences from Vue 3:

  • No <script setup>, no defineProps/defineEmits macros.
  • data() returns reactive state; access via this.x.
  • No Composition API by default (Vue 2.7 has it as opt-in via @vue/composition-api plugin).
  • State management: Vuex 3.x (store.commit, store.dispatch, mapState/mapActions helpers).
  • Router: vue-router v3.x.
  • Reactivity: Vue.set() / this.$set() for adding new keys to reactive objects (Vue 3 fixed this).

Migrate to Vue 3 only when BA spec explicitly asks. For new features in Vue 2 codebase, follow existing Options API patterns.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill