Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Vue3 Patterns

ASecurity

Vue 3 idioms — Composition API, `<script setup>`, composables, Pinia stores, reactivity gotchas, performance, and Volar / vue-tsc strictness. Auto-fires for `*.vue` and Vue-specific TypeScript files alongside `frontend-patterns` to add Vue depth the generic skill doesn't cover.

12 stars
0 votes
0 copies
0 views
Added 9/28/2026
developmenttypescriptgoreactvueangularrefactoringcode-reviewapifrontendsecurity

Works with

api

Security Analysis

A100/100

Scanned 9/28/2026

Install to Claude Code

$npx -y skills add Nmor/the-claude-council --skill vue3-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Vue3 Patterns?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Vue3 Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nmor-vue3-patterns/badge)](https://www.skillsdirectory.com/skills/nmor-vue3-patterns)

More formats (shields.io, HTML) on the badges page.

Files
SKILL.md
---
name: vue3-patterns
description: Vue 3 idioms — Composition API, `<script setup>`, composables, Pinia stores, reactivity gotchas, performance, and Volar / vue-tsc strictness. Auto-fires for `*.vue` and Vue-specific TypeScript files alongside `frontend-patterns` to add Vue depth the generic skill doesn't cover.
---

# Vue 3 Patterns

> **Reuse-first** (per `~/.claude/rules-library/common/reuse-first.md`):
> Before creating any new component, composable, store, or
> utility, sweep `components/`, `components/ui/`, `composables/`,
> `stores/`, `lib/`, `utils/`. One source of truth per primitive
> (one `<BaseButton>`, one `<DescriptionEditor>`, one
> `useApi()`, one `useToast()`). Extend with a prop or option —
> never fork. Compositional helpers (renderless components,
> `provide` / `inject`, slot-based APIs) are the canonical
> mechanism for extending shared primitives in Vue.
>
> **Size budget: 21 KB** — `token-budget.mjs --check`.

Vue 3 patterns that go beyond the generic `frontend-patterns` skill. Use
when the question is "how should this Vue component / composable / store be
shaped" — reactivity correctness, composable design, Pinia layout, and the
performance footguns specific to Vue's reactivity system.

## When to Activate

- Authoring or refactoring `.vue` files
- Designing a composable (`useFoo`)
- Adding or restructuring a Pinia store
- Reviewing a Vue PR for reactivity / performance bugs
- Tuning `vue-tsc` strictness or Volar config

## Default to `<script setup lang="ts">` + Composition API

Options API (`data() {}`, `methods: {}`) is fine for tiny demos and
discouraged for production code. Composition API + `<script setup>` is the
default because it composes naturally, plays well with TypeScript, and
eliminates `this`-binding bugs.

```vue
<script setup lang="ts">
import { ref, computed } from "vue";

const props = defineProps<{ userId: string }>();
const emit = defineEmits<{ saved: [userId: string] }>();

const draft = ref("");
const isValid = computed(() => draft.value.length > 0);

function save() {
  if (!isValid.value) return;
  emit("saved", props.userId);
}
</script>
```

Type-only `defineProps` / `defineEmits` give you better inference and
auto-complete than the runtime-array form.

## ref vs reactive: Default To `ref`

| API | Use when | Watch out for |
| --- | -------- | ------------- |
| `ref<T>` | Default for all reactive state | Always `.value` outside templates |
| `reactive<T>` | A bag of related fields you want to mutate by property | Destructuring breaks reactivity (`const { foo } = state`) |
| `shallowRef<T>` | Large objects you replace wholesale (D3 datasets, mxGraph models) | Inner mutations not tracked — replace the ref, don't mutate |
| `readonly<T>` | Public surface of a composable that owns the state | — |

Default to `ref`. Only reach for `reactive` when you've felt the pain of
`.value` more than the pain of "wait, why isn't this updating after I
destructured it."

## Composables: One File, Returns A Plain Object

A composable is a function starting with `use`, returning a plain object
of refs / computed / functions. Conventions:

```ts
// composables/useTask.ts
export function useTask(taskId: Ref<string>) {
  const task = ref<Task | null>(null);
  const loading = ref(false);

  async function load() {
    loading.value = true;
    task.value = await api.getTask(taskId.value);
    loading.value = false;
  }

  // Re-load when the input ref changes
  watch(taskId, load, { immediate: true });

  return { task: readonly(task), loading: readonly(loading), reload: load };
}
```

Rules:

- The composable owns its state — return `readonly` refs so callers can't
  mutate them by accident.
- Take refs as inputs, not raw values, when the input may change.
- Don't call composables conditionally — they must run unconditionally
  during component setup so cleanup hooks register.
- One file per composable. Easier to find, easier to test.

## Pinia Stores: `defineStore` With Setup Syntax

Use the setup-function form for parity with `<script setup>` ergonomics:

```ts
export const useAuthStore = defineStore("auth", () => {
  const user = ref<User | null>(null);
  const isAdmin = computed(() => user.value?.role === "admin");

  async function login(email: string, password: string) { /* ... */ }
  function logout() { user.value = null; }

  return { user, isAdmin, login, logout };
});
```

Rules:

- One store per domain (`useAuthStore`, `useTasksStore`). Don't make a
  monolithic `useAppStore` — it becomes the new Vuex root.
- TTL-cache reads inside the store (`if (Date.now() - lastFetch < 60_000) return;`).
  Components don't have to remember to debounce.
- Stores are global. Don't put per-route state there — use a composable
  scoped to the component that owns the route.
- Don't import a store inside another store's top-level — call it inside
  the action that needs it. Top-level imports create circular initialization.

## Reactivity Gotchas

These bite once and you remember forever. Better to read them now.

| Gotcha | Why | Fix |
| ------ | --- | --- |
| `const { foo } = reactive(state)` loses reactivity | `foo` is a plain value at the moment of destructure | Use `toRefs(state)` or keep `state.foo` access |
| Mutating an array prop directly | Props are readonly | Emit an event; let parent mutate |
| `ref` of a `Map` / `Set` doesn't trigger updates | Vue 3 has special handling for Map/Set; pre-3.0.11 it didn't | Replace the whole map: `m.value = new Map(m.value).set(k, v)` |
| `computed` with side effects | `computed` should be pure; effects belong in `watchEffect` | Move side effects to `watchEffect` or `watch` |
| `watch(source, cb)` not firing | Source isn't reactive (raw value) or watcher set up before mount | Pass a getter `() => source.value`, ensure watcher in `setup` |
| `v-for` over a `Map` doesn't update | Same as above; Vue tracks reference identity for collections | Use `Array.from(map.value)` or replace whole map |
| `unref(x)` on a non-ref returns `x` | Useful for "ref or value" inputs | `function setName(name: MaybeRef<string>) { const v = unref(name); ... }` |

## Performance: Where Vue Bleeds

Vue's reactivity is fast, but four patterns dominate slow apps:

1. **Big lists without virtualization** — 1000+ rows render every cell.
   Use `RecycleScroller` (`vue-virtual-scroller`) or `vue-virtual-list`
   for any list that can grow past 200 items.

2. **Computed dependency cycles** — a `computed` that reads another
   `computed` that reads the first. Vue detects and warns; fix the design.

3. **Deep watchers on big objects** — `watch(state, ..., { deep: true })`
   walks every property on every change. Replace with a watcher on a
   specific path, or restructure state into smaller refs.

4. **Component re-renders on parent state change** — Vue 3 is much better
   than Vue 2 here, but a `<HeavyComponent v-bind="$props" />` still
   re-renders on every parent change. Use `defineProps` precisely and
   memoize children with `v-memo` for expensive trees.

## `v-memo` For Expensive Trees

`v-memo` skips re-render when the dependency array hasn't changed.
Use sparingly — it's a manual optimization, easy to misuse:

```vue
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.updatedAt]">
  <!-- expensive -->
</div>
```

Only the listed deps trigger re-render. Forget to include one and you
get stale UI.

## Volar / vue-tsc Strictness

Pin Volar to the workspace TypeScript SDK so editor diagnostics match CI.
In `tsconfig.json`:

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "verbatimModuleSyntax": true,
    "moduleResolution": "Bundler",
    "skipLibCheck": true
  },
  "vueCompilerOptions": {
    "strictTemplates": true
  }
}
```

`strictTemplates` makes typos in template bindings (`{{ user.namee }}`) a
compile error. Worth the upfront pain.

CI runs `vue-tsc --noEmit` — same check the IDE shows. Drift between the
two means you're shipping bugs that look fine locally.

## Slots: Type Them

```vue
<script setup lang="ts">
defineSlots<{
  default(props: { task: Task }): unknown;
  empty(): unknown;
}>();
</script>

<template>
  <slot v-if="tasks.length" name="default" :task="tasks[0]" />
  <slot v-else name="empty" />
</template>
```

`defineSlots` gives consumers compile-time type-safety on slot props and
on which slots exist. Without it, slot misuse fails at runtime.

## Async Components For Routes

```ts
const RoadmapView = defineAsyncComponent(() => import("@/views/RoadmapView.vue"));
```

Or in the router:

```ts
{ path: "/roadmap", component: () => import("@/views/RoadmapView.vue") }
```

Keeps the initial bundle small. Combined with Vite's `manualChunks` for
heavy libs (mxGraph, Chart.js), the first paint stays fast even as the
app grows.

## Common Smells

| Smell | Fix |
| ----- | --- |
| `data() { return { ... } }` in a new file | Use `<script setup>` + `ref` |
| `this.$emit` (Options API) in 2024+ codebase | `defineEmits` typed events |
| `ref(somePropValue)` then never updating | Use a `computed`, or `watch` the prop |
| `reactive({ list: [], filters: {} })` then `const { list } = state` | Either `toRefs(state)` or use `ref` per field |
| Pinia `state: () => ({ ... })` + `actions: { ... }` (options form) | Setup form: closure over refs and functions |
| `v-for` without `:key` (or `:key="index"`) | Use a stable id: `:key="item.id"` |
| `:style="{...}"` rebuilt every render with the same object | `computed(() => ({ ... }))` |
| Watching deep state to derive a value | Replace with `computed` |

## Skill Chain

1. **frontend-patterns** — generic component / state / hook patterns
2. **vue3-patterns** — this skill (Vue-specific depth)
3. **frontend-design** — typography, color, motion, aesthetics
4. **typescript-patterns** — narrowing, branded types, exhaustiveness

## Purpose

Principal-level Vue 3 engineering: Composition API + `<script
setup>` as the canonical default, composables (the `useX()`
pattern) for reusable logic, Pinia for global state, reactivity
discipline (no destructuring `reactive()` — that loses
reactivity), `ref` vs `reactive` semantics, computed vs watchers,
provide / inject scoping, async setup + `<Suspense>`, Teleport
for portals, dynamic + async components for code-splitting,
defineModel for v-model, TypeScript + Volar for type safety, and
the SSR + hydration discipline (Nuxt 3 / Vite SSR).

**Negative scope** (NOT what this skill covers):

- Generic frontend patterns — see `frontend-patterns`
- Visual design quality (typography, colour, motion) — see
  `frontend-patterns` design section
- TypeScript depth — see `typescript-patterns`
- Vue 2 / Options API patterns — out (Vue 2 EOL 2024-Dec-31)
- React / Svelte / Angular — different frameworks

## When NOT to use

- Pure server-rendered HTML pages with no interactivity (use
  templates + a sprinkle of vanilla JS)
- Single-file widget embedded in another app's DOM (Vue's runtime
  cost is large relative to vanilla / Preact for tiny widgets)
- Existing Vue 2 codebase mid-migration (Composition API can be
  introduced gradually; full Vue 3 patterns require the runtime)

## Standards Cited

- **Vue 3 Documentation** (vuejs.org) — canonical reference
- **Vue Style Guide — Priority A/B/C/D** (vuejs.org/style-guide)
- **Pinia Documentation** (pinia.vuejs.org) — official state mgmt
- **Vue Router 4 Documentation** — routing
- **Nuxt 3 Documentation** (nuxt.com) — SSR + meta-framework
- **Volar / vue-tsc** — type checking
- **WHATWG HTML Living Standard** — base
- **WAI-ARIA 1.2** — accessibility primitives for custom components
- **WCAG 2.2** — accessibility floor (via `a11y.md`)
- **OWASP ASVS 4.0.3 §5.3 (Output Encoding + Injection Prevention)** —
  XSS guards (Vue's auto-escaping + sanitisation)
- **OWASP ASVS 4.0.3 §14.4 (HTTP Security Headers)** — CSP +
  framework integration

## Anti-Patterns

| Pattern | Why bad | Correct alternative |
| --- | --- | --- |
| Options API in new code | Inconsistent with team standard; harder to reuse logic | `<script setup>` + Composition API |
| Destructuring `reactive()` | Loses reactivity (silent bug) | Use `toRefs(state)` OR keep state as `reactive` and access via `state.x` |
| Mutating props directly | Vue dev mode warns; breaks one-way data flow | `emit('update:modelValue', x)` or `defineModel()` |
| `v-html` with user input | XSS vector | Render text content; or sanitise via DOMPurify before `v-html` |
| Inline event handler with complex logic | Untestable; coupling | Extract to method / composable |
| Watching reactive object without `deep: true` | Misses nested changes | Use `watch(source, fn, { deep: true })` OR `watchEffect` |
| `ref().value` reassign in template | Templates auto-unwrap; manual `.value` breaks reactivity | Use the ref directly in templates |
| Global state in module-level `ref()` | Singleton across tests / SSR (memory leak across requests) | Pinia store (per-app instance) |
| `setup()` with async work + no `<Suspense>` | Component hangs without fallback | Wrap in `<Suspense>` with `#fallback` |
| Component name collision with HTML | Some browsers ignore custom elements without `-` | Multi-word PascalCase / kebab-case |
| Provide / inject without `Symbol` keys | Name collision in larger apps | Typed `InjectionKey<T>` |
| List rendering with index as `:key` | Animation glitches on reorder | Stable id from data |
| Composable side effects in setup() not cleaned up | Leaks across HMR / hot reload | Use `onScopeDispose` / `onBeforeUnmount` |

## Verification Checklist

- [ ] Composition API + `<script setup>` used in all new files
- [ ] Volar + `vue-tsc --noEmit` returns zero errors
- [ ] No `reactive()` destructured without `toRefs()`
- [ ] No `v-html` with user-controlled input
- [ ] Composables extracted on second occurrence (rule of three)
- [ ] Global state via Pinia (per-app instance), never module-level
      `ref()`
- [ ] Lists keyed by stable id from data (S6479)
- [ ] Async setup wrapped in `<Suspense>` with fallback
- [ ] Provide / inject use typed `InjectionKey<T>` symbols
- [ ] Watchers use appropriate `deep` / `immediate` flags
- [ ] `defineModel()` (3.4+) for two-way binding
- [ ] Accessibility audited per `a11y.md` (ARIA, keyboard nav,
      focus management)
- [ ] No raw colour literals; design tokens via CSS vars
- [ ] SSR-safe (no `window` access outside `onMounted`)
- [ ] Bundle size budgeted (Vite chunk analysis)

## Cross-References

- `~/.claude/skills/frontend-patterns/SKILL.md` — generic patterns
- `~/.claude/skills/typescript-patterns/SKILL.md` — TS depth
- `~/.claude/skills/coding-quality-rules/SKILL.md` — universal baseline
- `~/.claude/rules-library/common/a11y.md` — accessibility floor
- `~/.claude/rules-library/common/no-discards.md` — Vue-specific S6299
  (no-bypass-sanitization) ban
- `~/.claude/rules-library/common/extreme-lint-policy.md` — eslint-plugin-vue
  - sonarjs strict
- `~/.claude/agents/code-reviewer.md` — Council Division 3
- `~/.claude/agents/ux-reviewer.md` — Council Division 7
- Vue 3 Style Guide (vuejs.org/style-guide)

## Why this skill exists

Vue 3's Composition API + `<script setup>` are the canonical
baseline for new Vue work — but the framework's flexibility means
teams ship Vue 2 idioms, reactive() destructuring bugs, `v-html`
XSS, and Pinia-vs-module-level-state confusion. The patterns above
codify the production-ready posture: Composition API, typed
composables, reactivity discipline, Pinia for global state,
SSR-safety, ARIA-correct components, design-token-based styling.
Teams that adopt these maintain Vue's velocity advantage; teams
that don't accumulate the silent-bug debt the reactivity system
hides behind interactive UIs.

## Learning hooks

Per `~/.claude/rules/common/continuous-learning-mandate.md`:

**Signals to watch**:

- Options API used in new code where Composition API + `<script setup>` is the canonical baseline
- `ref` / `reactive` misuse (e.g., destructuring reactive object loses reactivity — silent bug)
- `watchEffect` used where `watch` with explicit deps would be clearer
- Composable function not prefixed with `use` (Vue convention drift)
- Pinia store accessed inside `setup` without `storeToRefs` for destructuring (reactivity loss)
- Template directives misused (`v-if` + `v-for` on same element, deprecated `v-html` without
  sanitization)
- `provide` / `inject` used as a global state shortcut instead of Pinia
- Component prop typed as `any` / `Object` instead of typed interface
- `defineExpose` used to leak internals instead of defining a proper public API
- Reactivity gotcha: mutating a `readonly` prop / mutating arrays / Maps without `.value`

**Refinement candidates**:

- New idiom row when Vue ships a new release (e.g., Vapor mode, new macros)
- Tightening of the `<script setup>` enforcement when Options API recurs in new code
- New cross-reference when a sister skill (frontend-patterns, typescript-patterns, accessible-forms)
  adds a Vue-relevant gate
- New composable template when a recurring shared pattern emerges (e.g., `useDebounce`,
  `usePagination`)

Attribution

NmorNmor
View sourceMore from Nmor →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

284972 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2222 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

10311 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →