All skills
hyf0 avatar

/vue-best-practices

@b9d14d0 official
by hyf0hyf0/vue-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/hyf0/vue-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
  • vue3
  • best-practices
  • component-design
  • reactivity
  • vite

README badge

README badge for hyf0/vue-skills/vue-best-practices

Provides a structured workflow for Vue 3 development using Composition API with `<script setup>` and TypeScript. Covers component splitting, reactivity patterns, data flow, composables, and optional features like transitions and async components, with performance optimization as a post-functionality pass.

Generated from the current SKILL.md.

Does this skill work with Vue 2 or only Vue 3?
This skill targets Vue 3 with Composition API and `<script setup>`. For Vue 2 or Options API projects, it recommends loading alternative skills if available.
Should I use Options API or Composition API with this skill?
Always use Composition API with `<script setup>` and TypeScript unless the project explicitly requires Options API, in which case you should load the `vue-options-api-best-practices` skill instead.
Does this skill provide reference documentation or just instructions?
The skill includes must-read references covering reactivity, SFC structure, component data flow, and composables that should be kept in active context throughout your work.
When should I add features like slots, Teleport, or Transition?
These optional features should only be loaded when your requirements explicitly call for them. The skill defaults to simpler patterns and treats advanced features as post-functionality additions.
Does this skill require TypeScript?
Yes. The skill strongly recommends TypeScript as part of the standard Composition API + `<script setup>` approach for Vue 3.

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