All skills
vuejs-ai avatar

/vue-best-practices

@b9d14d0 official
by Vue AIvuejs-ai/skills2.9k stars
167

MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.

Use this Skill: https://skilld.dev/gh/vuejs-ai/skills/vue-best-practices

This session only. Nothing lands on disk.

referencescomponent-data-flow.md

≈1.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Component Data Flow Best Practices

Impact: HIGH - Vue components stay reliable when data flow is explicit: props go down, events go up, v-model handles two-way bindings, and provide/inject supports cross-tree dependencies. Blurring these boundaries leads to stale state, hidden coupling, and hard-to-debug UI.

The main principle of data flow in Vue.js is Props Down / Events Up. This is the most maintainable default, and one-way flow scales well.

Task List

  • Treat props as read-only inputs
  • Use props/emit for component communication; reserve refs for imperative actions
  • When refs are required for imperative APIs, type them with template refs
  • Emit events instead of mutating parent state directly
  • Use defineModel for v-model in modern Vue (3.4+)
  • Handle v-model modifiers deliberately in child components
  • Use symbols for provide/inject keys to avoid props drilling (over ~3 layers)
  • Keep mutations in the provider or expose explicit actions
  • In TypeScript projects, prefer type-based defineProps, defineEmits, and InjectionKey

Props: One-Way Data Down

Props are inputs. Do not mutate them in the child.

BAD:

<script setup>
const props = defineProps({ count: Number })

function increment() {
  props.count++
}
</script>

GOOD:

If state needs to change, emit an event, use v-model or create a local copy.

Prefer props/emit over component refs

BAD:

<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'

const formRef = ref(null)

function submitForm() {
  if (formRef.value.isValid) {
    formRef.value.submit()
  }
}
</script>

<template>
  <UserForm ref="formRef" />
  <button @click="submitForm">Submit</button>
</template>

GOOD:

<script setup>
import UserForm from './UserForm.vue'

function handleSubmit(formData) {
  api.submit(formData)
}
</script>

<template>
  <UserForm @submit="handleSubmit" />
</template>

Type component refs when imperative access is required

Prefer props/emits by default. When a parent must call an exposed child method, type the ref explicitly and expose only the intended API from the child with defineExpose.

BAD:

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DialogPanel from './DialogPanel.vue'

const panelRef = ref(null)

onMounted(() => {
  panelRef.value.open()
})
</script>

<template>
  <DialogPanel ref="panelRef" />
</template>

GOOD:

<!-- DialogPanel.vue -->
<script setup lang="ts">
function open() {}

defineExpose({ open })
</script>
<!-- Parent.vue -->
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
import DialogPanel from './DialogPanel.vue'

// Vue 3.5+ with useTemplateRef
const panelRef = useTemplateRef('panelRef')

// Before Vue 3.5 with manual typing and ref
// const panelRef = ref<InstanceType<typeof DialogPanel> | null>(null)

onMounted(() => {
  panelRef.value?.open()
})
</script>

<template>
  <DialogPanel ref="panelRef" />
</template>

Emits: Explicit Events Up

Component events do not bubble. If a parent needs to know about an event, re-emit it explicitly.

BAD:

<!-- Parent expects "saved" from grandchild, but it won't bubble -->
<Child @saved="onSaved" />

GOOD:

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['saved'])

function onGrandchildSaved(payload) {
  emit('saved', payload)
}
</script>

<template>
  <Grandchild @saved="onGrandchildSaved" />
</template>

Event naming: use kebab-case in templates and camelCase in script:

<script setup>
const emit = defineEmits(['updateUser'])
</script>

<template>
  <ProfileForm @update-user="emit('updateUser', $event)" />
</template>

v-model: Predictable Two-Way Bindings

Use defineModel by default for component bindings and emit updates on input. Only use the modelValue + update:modelValue pattern if you are on Vue < 3.4.

BAD:

<script setup>
const props = defineProps({ value: String })
</script>

<template>
  <input :value="props.value" @input="$emit('input', $event.target.value)" />
</template>

GOOD (Vue 3.4+):

<script setup>
const model = defineModel({ type: String })
</script>

<template>
  <input v-model="model" />
</template>

GOOD (Vue < 3.4):

<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

If you need the updated value immediately after a change, use the input event value or nextTick in the parent.

Provide/Inject: Shared Context Without Prop Drilling

Use provide/inject for cross-tree state, but keep mutations centralized in the provider and expose explicit actions.

BAD:

// Provider.vue
provide('theme', reactive({ dark: false }))

// Consumer.vue
const theme = inject('theme')
// Mutating shared state from any depth becomes hard to track
theme.dark = true

GOOD:

// Provider.vue
const theme = reactive({ dark: false })
const toggleTheme = () => { theme.dark = !theme.dark }

provide(themeKey, readonly(theme))
provide(themeActionsKey, { toggleTheme })

// Consumer.vue
const theme = inject(themeKey)
const { toggleTheme } = inject(themeActionsKey)

Use symbols for keys to avoid collisions in large apps:

export const themeKey = Symbol('theme')
export const themeActionsKey = Symbol('theme-actions')

Use TypeScript Contracts for Public Component APIs

In TypeScript projects, type component boundaries directly with defineProps, defineEmits, and InjectionKey so invalid payloads and mismatched injections fail at compile time.

BAD:

<script setup lang="ts">
import { inject } from 'vue'

const props = defineProps({
  userId: String
})

const emit = defineEmits(['save'])
const settings = inject('settings')

// Payload shape is not checked here
emit('save', 123)

// Key is string-based and not type-safe
settings?.theme = 'dark'
</script>

GOOD:

<script setup lang="ts">
import { inject, provide } from 'vue'
import type { InjectionKey } from 'vue'

interface Props {
  userId: string
}

interface Emits {
  save: [payload: { id: string; draft: boolean }]
}

interface Settings {
  theme: 'light' | 'dark'
}

const settingsKey: InjectionKey<Settings> = Symbol('settings')

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

provide(settingsKey, { theme: 'light' })

const settings = inject(settingsKey)
if (settings) {
  emit('save', { id: props.userId, draft: false })
}
</script>

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides a comprehensive set of best practices for Vue.js development, focusing on the Composition API, reactivity, and performance optimization. It includes standard coding guidelines and explicitly recommends security best practices such as sanitizing untrusted content to prevent XSS.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    23 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at b9d14d0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 8 months ago
metadata
{
  "author": "github.com/vuejs-ai",
  "version": "18.0.0"
}
  • Vue
  • TypeScript
  • composition-api
  • script-setup
  • vue-router
  • pinia
  • vite
  • ssr
  • volar
  • component-design

README badge

README badge for vuejs-ai/skills/vue-best-practices

Instructs Claude to follow Vue 3 best practices using Composition API with `<script setup>` and TypeScript, covering reactivity, component splitting, data flow, and composables. Load this skill for any Vue work—it enforces a workflow that prioritizes minimal state, explicit props/events, focused components, and defers optional features until requirements demand them.

Generated from the current SKILL.md.

Should I use Options API or Composition API?
Use Composition API with <script setup> and TypeScript as the default. Only switch to Options API if the project explicitly requires it.
Does this skill cover Vue 2?
No. This skill targets Vue 3, Composition API, <script setup>, and TypeScript.
When should I split a component into smaller components?
Split when a component has more than one clear responsibility, contains 3+ distinct UI sections, or has repeated template blocks. Keep entry/root and route view components thin unless the feature is a small throwaway demo.
What references should I read before starting a Vue task?
You must read and keep active: reactivity.md, sfc.md, component-data-flow.md, and composables.md. Load additional optional references only when requirements call for slots, animations, state management, or other specific features.
When should I add state management like Pinia?
Only when app-wide shared state crosses feature boundaries. For most features, use composables and props/events for data flow instead.

Generated from the current SKILL.md. These answers refresh after source changes.