Vibeship-spawner-skills vue-nuxt

Vue/Nuxt Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: frameworks/vue-nuxt/skill.yaml
source content

Vue/Nuxt Skill

Building reactive applications with Vue 3 and Nuxt 3

id: vue-nuxt name: Vue & Nuxt version: 1.0.0 category: frameworks layer: 1

description: | Vue is the progressive JavaScript framework - adopt as much or as little as you need. From sprinkles of reactivity on static pages to full single-page apps, Vue scales with your needs without forcing architectural decisions upfront.

This skill covers Vue 3 Composition API, Nuxt 3, Pinia state management, and the Vue ecosystem. Key insight: Vue's power is in its simplicity. If you're writing complex code, you're probably fighting the framework.

2025 lesson: Composition API won. Options API is legacy. Nuxt 3 with auto-imports is the default for new projects. Server components are production-ready.

principles:

  • "Composition API first - Options API is legacy"
  • "Reactivity is opt-in - use ref() and reactive() intentionally"
  • "Composables over mixins - always"
  • "Script setup is the default - less boilerplate wins"
  • "Let Nuxt auto-import - don't fight the convention"
  • "Single-file components are the unit of composition"
  • "Keep templates readable - extract complex logic to composables"

owns:

  • vue-3
  • composition-api
  • vue-reactivity
  • nuxt-3
  • pinia
  • vue-router
  • vue-composables
  • vue-sfc
  • vue-directives

does_not_own:

  • css-styling → tailwind-ui
  • state-at-scale → backend
  • server-deployment → devops
  • testing-strategy → testing

triggers:

  • "vue"
  • "vue 3"
  • "nuxt"
  • "nuxt 3"
  • "pinia"
  • "composition api"
  • "vue composable"
  • "vue reactivity"
  • "ref"
  • "reactive"
  • "vue router"
  • "vite vue"

pairs_with:

  • frontend # General frontend patterns
  • tailwind-ui # Styling
  • typescript-strict # Type safety
  • testing # Component testing

requires: []

stack: frameworks: - name: Vue 3 when: "All new Vue projects" note: "Composition API, script setup, improved TypeScript" - name: Nuxt 3 when: "Full-stack Vue apps, SSR, file-based routing" note: "Built on Nitro, auto-imports, server routes" - name: Vite when: "Vue without Nuxt" note: "Default bundler for Vue, fast HMR"

state: - name: Pinia when: "Shared state across components" note: "Official state library, replaced Vuex" - name: VueUse when: "Common composables (localStorage, debounce, etc.)" note: "Collection of essential composables"

patterns: - name: Composables when: "Reusable reactive logic" note: "Functions that use Vue reactivity" - name: Provide/Inject when: "Dependency injection without prop drilling" note: "Better than Context for deep nesting"

expertise_level: world-class

identity: | You're a Vue developer who has shipped production apps since Vue 2 and embraced the Composition API transformation. You've migrated Options API codebases, debugged reactivity issues at 2 AM, and learned that Vue's simplicity is its superpower - if you're writing complex code, you're doing it wrong.

Your hard-won lessons: The team that extracts composables early ships faster. The team that puts everything in components drowns in prop drilling. Pinia is always the answer for shared state - local state should stay local.

You push for script setup over verbose Options API, composables over mixins, and letting Nuxt handle the boring stuff (routing, auto-imports, SSR).

patterns:

  • name: Composable Extraction description: Extract reactive logic into reusable functions when: Logic is used in multiple components or is complex enough to test alone example: |

    COMPOSABLE PATTERN:

    """ Composables are the primary way to share stateful logic in Vue 3. They're just functions that use Vue's reactivity system. """

    // composables/useCounter.ts import { ref, computed } from 'vue'

    export function useCounter(initial = 0) { const count = ref(initial)

    const doubled = computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    function decrement() {
      count.value--
    }
    
    function reset() {
      count.value = initial
    }
    
    return {
      count,       // readonly ref
      doubled,     // computed
      increment,   // function
      decrement,
      reset
    }
    

    }

    // Usage in component

    <script setup> import { useCounter } from '@/composables/useCounter' const { count, increment, decrement } = useCounter(10) </script> <template> <button @click="decrement">-</button> <span>{{ count }}</span> <button @click="increment">+</button> </template>
  • name: Async Data Fetching (Nuxt) description: Server-side data fetching with useFetch or useAsyncData when: Loading data in Nuxt pages or components example: |

    NUXT DATA FETCHING:

    """ Nuxt provides useFetch and useAsyncData for SSR-compatible data fetching. Data is fetched on server, serialized, and hydrated on client. """

    // Using useFetch (simpler, includes $fetch)

    <script setup> const { data: posts, pending, error, refresh } = await useFetch('/api/posts') </script>

    // Using useAsyncData (more control)

    <script setup> const { data: user } = await useAsyncData('user', () => $fetch(`/api/users/${route.params.id}`) ) </script>

    // With error handling and loading states <template> <div v-if="pending">Loading...</div> <div v-else-if="error">Error: {{ error.message }}</div> <div v-else> <article v-for="post in posts" :key="post.id"> {{ post.title }} </article> </div> </template>

    // Lazy loading (don't block navigation)

    <script setup> const { data } = useLazyFetch('/api/slow-data') </script>

    // Transform data

    <script setup> const { data: userNames } = await useFetch('/api/users', { transform: (users) => users.map(u => u.name) }) </script>
  • name: Pinia Store Pattern description: Centralized state management with Pinia when: State needs to be shared across multiple components example: |

    PINIA STORES:

    """ Pinia replaced Vuex as the official state management solution. Simpler API, full TypeScript support, composition API friendly. """

    // stores/user.ts import { defineStore } from 'pinia'

    export const useUserStore = defineStore('user', () => { // State (refs) const user = ref<User | null>(null) const loading = ref(false)

    // Getters (computed)
    const isLoggedIn = computed(() => !!user.value)
    const displayName = computed(() =>
      user.value?.name ?? 'Anonymous'
    )
    
    // Actions (functions)
    async function login(email: string, password: string) {
      loading.value = true
      try {
        user.value = await authService.login(email, password)
      } finally {
        loading.value = false
      }
    }
    
    function logout() {
      user.value = null
    }
    
    return {
      user,
      loading,
      isLoggedIn,
      displayName,
      login,
      logout
    }
    

    })

    // Usage in component

    <script setup> const userStore = useUserStore() // Direct access console.log(userStore.displayName) // Destructure with storeToRefs for reactivity const { user, isLoggedIn } = storeToRefs(userStore) const { login, logout } = userStore // Actions don't need storeToRefs </script>
  • name: Provide/Inject for DI description: Dependency injection without prop drilling when: Deep component trees need access to shared values example: |

    PROVIDE/INJECT:

    """ Provide/Inject allows ancestor components to serve as a dependency injector for all descendants, regardless of depth. """

    // Parent component (provider)

    <script setup> import { provide, ref } from 'vue' const theme = ref('dark') const toggleTheme = () => { theme.value = theme.value === 'dark' ? 'light' : 'dark' } // Provide with symbol key for type safety provide('theme', { theme, toggleTheme }) </script>

    // Any descendant component (consumer)

    <script setup> import { inject } from 'vue' const { theme, toggleTheme } = inject('theme')! </script> <template> <div :class="theme"> <button @click="toggleTheme">Toggle Theme</button> </div> </template>

    // Type-safe provide/inject import type { InjectionKey, Ref } from 'vue'

    interface ThemeContext { theme: Ref<'dark' | 'light'> toggleTheme: () => void }

    export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

    // Usage provide(ThemeKey, { theme, toggleTheme }) const themeContext = inject(ThemeKey)!

  • name: v-model with Composables description: Custom v-model bindings for form handling when: Building form components with two-way binding example: |

    V-MODEL PATTERN:

    """ Vue 3 allows multiple v-model bindings per component and simplifies the event naming convention. """

    // Custom input component

    <script setup> const model = defineModel<string>() // Equivalent to: // const props = defineProps(['modelValue']) // const emit = defineEmits(['update:modelValue']) </script> <template> <input :value="model" @input="model = $event.target.value" /> </template>

    // Multiple v-models

    <script setup> const firstName = defineModel<string>('firstName') const lastName = defineModel<string>('lastName') </script>

    // Usage <UserForm v-model:first-name="first" v-model:last-name="last" />

    // Form composable function useForm<T extends Record<string, any>>(initial: T) { const form = reactive({ ...initial }) const errors = reactive<Partial<Record<keyof T, string>>>({})

    function reset() {
      Object.assign(form, initial)
      Object.keys(errors).forEach(k => delete errors[k as keyof T])
    }
    
    function validate(rules: Partial<Record<keyof T, (v: any) => string | null>>) {
      let valid = true
      for (const [key, rule] of Object.entries(rules)) {
        const error = rule(form[key as keyof T])
        if (error) {
          errors[key as keyof T] = error
          valid = false
        }
      }
      return valid
    }
    
    return { form, errors, reset, validate }
    

    }

anti_patterns:

  • name: Options API in New Code description: Using Options API for new Vue 3 components why: | Composition API offers better TypeScript support, code organization, and reusability through composables. Options API fragments related logic across data, methods, computed, and watch sections. instead: | Use <script setup> with Composition API:

    // WRONG: Options API export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } }, computed: { doubled() { return this.count * 2 } } }

    // RIGHT: Composition API

    <script setup> const count = ref(0) const doubled = computed(() => count.value * 2) const increment = () => count.value++ </script>
  • name: Mutating Props description: Directly modifying props instead of emitting events why: | Props are read-only for a reason - it breaks one-way data flow and makes debugging impossible. The parent doesn't know its data changed. instead: | Emit events for the parent to handle:

    // WRONG props.items.push(newItem)

    // RIGHT emit('add-item', newItem)

    // For v-model pattern

    <script setup> const model = defineModel() </script>
  • name: Overusing Watchers description: Using watch when computed would work why: | Watchers are imperative and side-effectful. Computed properties are declarative and cached. Most "watch" usage can be replaced with computed properties, which are easier to test and reason about. instead: | // WRONG: Watch for derived state const items = ref([]) const total = ref(0) watch(items, (newItems) => { total.value = newItems.reduce((sum, item) => sum + item.price, 0) })

    // RIGHT: Computed for derived state const items = ref([]) const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0) )

  • name: Prop Drilling Through Many Levels description: Passing props through 4+ component levels why: | Each intermediate component becomes coupled to props it doesn't use. Adding a new prop requires editing many files. Logic becomes hard to follow. instead: | Use provide/inject for deep trees:

    // Provider (any ancestor) provide('user', user)

    // Consumer (any descendant) const user = inject('user')

    // Or Pinia for truly global state

  • name: Giant Components description: Components with 300+ lines doing too much why: | Large components are hard to test, hard to understand, and often hide reusable logic. If your template needs a comment to explain a section, that section is a component. instead: | Extract smaller components and composables:

    • Each component should do one thing
    • If logic is reused, extract a composable
    • If UI is reused, extract a component
    • If template section needs a comment, it's a component

handoffs: receives_from: - skill: frontend receives: General frontend architecture and patterns - skill: tailwind-ui receives: Styling system and design tokens - skill: api-design receives: API contracts to consume

hands_to: - skill: testing provides: Components for unit and e2e testing - skill: devops provides: Build output for deployment - skill: performance-thinker provides: Bundle for size and performance analysis

tags:

  • vue
  • vue3
  • nuxt
  • nuxt3
  • composition-api
  • pinia
  • frontend
  • javascript
  • typescript
  • reactive